Yesterday was our first Nuxeo Tech Talks, held in our Paris Office. The goal of these Meetups is to talk about a technology used by Nuxeo developers. This time we invited Mathieu Robin to talk about “JavaScript without Bugs,” and our CTO Thierry Delprat talked about JavaScript in the Nuxeo Platform.
As Thierry said in his presentation, Nuxeo is a Java shop. Some of us are not completely up to date with the latest evolutions, tools etc… And since we’re going to do a lot more JavaScript, we thought it would be a very good topic for the first Nuxeo Tech Talk.
Mathieu’s presentation was basically about what can seem/is weird in the language. You know like the scope of the this object… I also discovered that you could throw Exceptions in JavaScript just like Java. Then he showed us some of the tools available to ease the pain of writing JS code. I m sure we’ll find a lot of interesting things when we setup JSHint. I invite you to check out his slidedeck on his GitHub repository.
Image may be NSFW. Clik here to view.Then Thierry went on with JavaScript@Nuxeo. He gave his thoughts about JavaScript as a language and then explained how it’s currently used and will be used in the Nuxeo Platform. You can expect a lot of changes around this, one of the first being nuxeo.js. It’s here to wrap our REST API in a nice way. Take a look at the slide deck at the bottom for the details. And you can see the JS shell he developed on the presentation he gave at Nuxeo World 2013.
Now if you want to hear about a specific technology related to Nuxeo, tell us and we’ll schedule a tech talk about this. And if you want to get updates about the next sessions, make sure you join the meetup group!
A big thanks to Mathieu, who was our first speaker and to all the people that came to this first Nuxeo Tech Talk. I hope we’ll see you at the next one :)
Two weeks ago I started a blog post series on how to create a thumbnail browsing module with AngularJS and our REST API. The first part was about getting familiar with the API. The second part was how to use it in AngularJS. The example was simple. A user goes to a URL with a tag and we display the search result with infinite scroll.
This third part focuses on getting the thumbnail with Busines Object adapters, making it look better, and deploying the project easily on Nuxeo.
Using the Business Object
In the first post we saw how to use a business object adapter with the new API. It’s really easy to plug into our app. All we have to do is tell the controller:
.controller("SlideshowCtrl",
['$scope','$routeParams','nxSearch'
($scope,$routeParams,nxSearch) ->
$scope.searchTag = $routeParams.tag
nxSearch.setQuery("SELECT * FROM Document WHERE ecm:tag = '" + $scope.searchTag + "'")
# set the BOAdapter using its name
nxSearch.setBOAdapter("SlideShowElement")
$scope.items = nxSearch.items
# Expose the busy variable
$scope.busy = nxSearch.busy
# Expose the nextPage method
$scope.nextPage = ()->
nxSearch.nextPage()
])
This works because the nxSearch service was made to support this capability. All we have to do is add a bo object, a setter and one line of code in our nextPage method: if nxSearch.bo? then url += "/@bo/" + nxSearch.bo
angular.module('nuxeoAngularSampleApp')
.factory "nxSearch", ["$http","$q","nxUrl",($http,$q,nxUrl)->
nxSearch = {}
nxSearch.items = []
nxSearch.busy = false
nxSearch.isNextPageAvailable = true
nxSearch.currentPageIndex = 0
nxSearch.pageSize = 20
nxSearch.query = undefined
nxSearch.bo = undefined
nxSearch.setBOAdapter = (bo)->
nxSearch.bo = bo
nxSearch.setPageSize = (pageSize)->
nxSearch.pageSize = pageSize
nxSearch.setQuery = (query)->
nxSearch.query = query
nxSearch.nextPage = ()->
if !nxSearch.query?
$q.reject("You need to set a query")
return
if !nxSearch.isNextPageAvailable
return
if nxSearch.busy
return
nxSearch.busy = true
url = nxUrl + "/path/@search"
# if the bo object exist, add the @bo adapter to the URL
if nxSearch.bo? then url += "/@bo/" + nxSearch.bo
url += "?currentPageIndex="+nxSearch.currentPageIndex+"&pageSize="+nxSearch.pageSize+"&query=" + nxSearch.query;
$http.get(url).then (response) ->
docs = response.data
if(angular.isArray(docs.entries))
nxSearch.currentPageIndex = docs.currentPageIndex + 1
nxSearch.isNextPageAvailable = docs.isNextPageAvailable
nxSearch.items.push doc for doc in docs.entries
nxSearch.busy = false
else
nxSearch.busy = false
$q.reject("just because")
nxSearch
]
And of course now we need to update our slideshow.html template to reflect these changes. I’ve added a div tag containing an img tag that shows our thumbnail. Note that we have to use item.value instead of simply item because we are manipulating business objects instead of simple documents. We might try to be more consistent in the next version of the API. Let me remind you that it’s still a beta version, so some things might still change a bit.
It looks a little raw, so we can use semantic-ui to make it nicer – it’s a little more WYSIWYM than bootstrap. Check out a comparison of the two on their home page. To install semantic-ui, we use bower and simply type:
bower install semantic-ui --save
The module is installed, but we still need to add the css stylesheet to our index.html page. We added this to our header:
But now we have bootstrap, which was already in the sample we started working on, and semantic-ui. So let’s clean this up. We can also remove all the parts we don’t use like the nxNavigation and nxSession services, or the controllers and pages we don’t use. We already did this work so you can check out the sources of the cleaned and package version on Damien’s GitHub repository.
About Packaging
Our goal is to deploy the angular project into nuxeo.war. To go further, we want to package the AngularJS app in a jar to deploy it in Nuxeo. The tool responsible for packaging the jar is Maven. We need to tell Maven to embed the app. To do so, we use the maven antrun plugin like this:
This configuration executes the ant using the ${basedir}/src/main/yo/build.xml ant build file. The build file actually calls Grunt to package the app. Grunt is a JavaScript task runner. As you can see in this file, ant executes the grunt build command.
Now when we run mvn clean install, we get a jar file (target/nuxeo-slideshow-sample*.jar) containing everything we need. Take a look at the content of the jar:
There are two XML files I have not talked about yet. Let’s start with deployment-fragment.xml. It does two things. First, it adds the URL pattern /slideshow/* to the NuxeoAuthenticationFilter. This is to make sure authentication is required when going to URLs like /slideshow/*. Next, unzip the app in nuxeo.war.
<?xml version="1.0"?>
<fragment version="1">
<extension target="web#STD-AUTH-FILTER">
<filter-mapping>
<filter-name>NuxeoAuthenticationFilter</filter-name>
<url-pattern>/slideshow/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
</extension>
<install>
<!-- Unzip the contents of our nuxeo.war into the server -->
<unzip from="${bundle.fileName}" to="/" prefix="web">
<include>web/nuxeo.war/**</include>
</unzip>
</install>
</fragment>
Now about authentication-contrib.xml. It defines slideshow as a startURLPattern. A start URL pattern is used the first time you browse the application. If you hit a URL for which a pattern is registered, you’ll see the login screen asking you to sign in. And if you do so, you’re redirected to the URL you’ve hit in the first place. If the URL you try to hit does not match any startURLPattern, you are still redirected to the login page, but once you sign in, you’re redirected to the default webapp.
And now we’re ready to go. We can build the jar and copy it in our bundles directory. Then we go to http://localhost:8080/nuxeo/slideshow/ and try our app deployed in Nuxeo.
This works because you have access to most of the objects available in Nuxeo’s classpath.
Just make sure log4j is configured according to the name of your logger. Here I’ve chosen >RunScript so I have to add the following line to my $NUXEO_HOME/lib/log4j.xml file.
The new UI Style Guide for the Nuxeo Platform 5.8 is fresh and released!
This guide is an online demo to help you developers and designers learn how to design your platform interface.
We published a post for the first version explaining what’s inside and why you should follow it. A second post explained why a style guide is important for developers as well for designers when building a product.
What’s New in the Guide?
There are a few changes, including:
Tables: New options are available to design your tables and move from a simple listing to beautiful data organization.
Messages: Feedback messages are fully reviewed, status messages have been updated and new classes are available to highlight elements in your content.
Buttons: The classes are more accurate to adjust your buttons and lead the user to the logical action.
Navigation: The styles of tabs, modular tabs, subtabs and breadcrumbs are now more complete to improve the navigation experience.
Boxes: New styles are availables to improve the widgets, layouts and tab content display: grayBoxes, simpleBoxes and .textBlock.
Consistency, Maintainability, Compatibility, Yes!
This guide’s role is to improve consistency in UI patterns and usability in your interface for the key users of your product. But as we know, behind every product there are developers with their concerns, so the guide’s role is also to be a shield for maintainability and compatibility.
The great news is that even if styles have evolved to be sleeker, more robust and complete, we garantee you that no styles will be broken during your upgrade from the previous version to the new Nuxeo Platfrom 5.8.
That is the bonus effect. You feel more relaxed now, right?
And the cool thing about this answer is that it comes from brian, another active community member. Here’s what he has to say:
Say you have document types SampleCustomPicture, CustomVideo, and CustomAudio — under “Projects>Advanced Settings>XML Extensions” do “+New” and create an extension with this content:
Kudos to Brian for providing the answer. Now how does it work? Well, when you drag and drop a file in Nuxeo, the plugins extension point is used. It lets you define a Java class to handle the import logic. Each plugin has an associated document type, some mime types and an order. The plugin with the lowest order and matching mime type is used. The plugin can either return a document or nothing. If it’s a document, the import is done. If it’s nothing, the next in order plugin is called.
Here all you have to do is modify the docType attribute to make sure your document types are used. Don’t forget the require tag to make sure your contribution will be deployed after the one you want to override.
If you want to go further, I recommend reading this blog post explaining how to create your own plugin implementation. This can help a lot especially for custom metadata you might have defined with Nuxeo Studio.
We have the first level of pluggability for WebEngine via the Fragments system. Unfortunately, we cannot at the same time contribute a WebEngine fragments to an extension point.
For now, we have to use two different modules, but:
It does not make sense
It can be tricky
The task to fix this problem is probably for Stephane: NXP-13283
REST Binding TCK
The current TCK page we have is not a ‘real’ TCK, meaning you cannot run the tests from it. And it is not really documentation either. But it can be useful for someone that wants to understand how Nuxeo HTTP APIs work. We can probably do better.
Damien suggested to use fitnesse to have readable tests. So far we did not find a way to use it for that purpose (validating a client), but any volunteers or ideas are welcome.
iOS SDK
Arnaud is working on the iOS connector with our friends from Smart&Soft.
API Bindings
We have an initial binding in Objective-C with:
REST Document endpoint
Operation API
The binding no longer uses RESTKit, but a bare HTTP lib (ASI Http). The API returns Document object wrapping of bare JSON objects.
Next, we’ll work on making the connector able to manage cache for offline usage.
Adding the Cache
ASI Request allows us to easily hook the HTTP request/response to add caching. But, this is still to be done:
Hook caching in HTTP layer
Manage storage (SQLLite for structure + Blob for response)
Tree Cache
Some apps will manage a tree of documents. This tree needs to be accessible offline. We have 2 options:
Retrieve the tree using n recursive 'getChildren' requests
We can use the standard request level caching
Retrieve the tree in one query
We need to introduce a dedicated API and a higher level of cache
NB: we should be able to compare with the Android logic that uses a “per list caching logic”.
API Playground
We should work on building a nice API playground, something like a hybrid between:
Nuxeo Explorer
The Automation Doc
The REST API doc
We’ll also have to define what is the target server for the playground:
For nuxeo.io users/customers, the target is clear: their instances
For other users that just want to take a look
Use a central site like demo.nuxeo.com?
Use Docker and a Core Server to provision on demand short lived instances?
Provide an AMI that people can use with a free AWS account?
Nuxeo IDE
Nuxeo IDE 1.2
Vlad is working on a new version of Nuxeo IDE.
IDE and Studio Integration
Anahide added a new registry on the Studio side so that the IDE can export operation without overriding existing configuration NXS-1242. For the multi-bundles use case, this is something we should probably handle on the IDE side (merge operation from all bundles before sending to Studio).
Make Nuxeo Less Stateful
Current Java API and WebFramework have inherited a stateful nature (EJB3 SFSB + JSF). With 5.9, it is time to start cleanup and lighten our framework.
Better Core Session Management
The stateful nature of the repository API creates concurrency issues. Rather that focusing on shielding problems of session sharing, we should work on making it almost stateless. It will be a much better solution.
The solution is to disconnect the DocumentModel object and make the reconnect dynamic. This implies:
We also need to discuss how we can make DocumentModel ThreadSafe. While doing this cleanup work, it will logically push our code to rely heavily on transaction demarcation. As a result, we may have to migrate all tests in Transactional mode:
Reduce Seam Beans Scopes
If we better manage CoreSession state, we should be able to make all our bean short lived:
This would reduce the memory footprint
We could remove part of the code to manage invalidations
Ben played a lot recently with the ReadACL system. He also implemented a new Simpler ReadACL system that allows you to skip one indirection in the security checking process.
It should be faster, but the trade-off is that you cannot have negative permissions: only positive permission and block all. So far, the tests have not shown a direct improvement, but still:
We will add this mode for PGSQL even if we don’t activate it by default
We will add global switch (nuxeo.properties) to activate simple ACL mode
Query and Big Sorting
We have some PageProvider queries that use sort on a nullable field. It looks like in this case, PGSQL cannot efficiently use the index. It was fixed by making the PageProvider implementation automatically add the not null clause in the NXQL Query.
Full text/TS_Vector issue
Since 5.7, in PGSQL, we store the full text in clear (not ‘vertorized’) inside the database: this is required to allow “phrase search”. Unfortunately, not storing directly the TS Vector makes the search query slower as the number of rows grows.
Options are:
Store only the TS_vector
Drop phrase search
Store TS_Vector and plain text
Make storage bigger
We need to:
Document the options
Explain the alternatives
Provide some migration tools
At least SQL scripts
NB: the real solution is to get the full text out of the SQL DB and use Elastic Search, but that’s for later.
Automation Server
Automation Marshaling Issue
The ResponseHelper used by default in Automation needs to be reviewed so that we avoid hard-coding marshaling logic. As long has we have this hardcoded class: adding a JAX-RS ResponseWriter also means updating this class.
Exception Handler is Different Between WebEngine and Automation
Automation Exception management has been improved, but we still have some specific processing that should not be here.
Returning 404 when a document is not found does not make sense in the context of an Operation
404 should mean resource not found => operation or chain is not found
We sometimes return 403 instead of 401
This means we need to review how we manage errors and associated HTTP status.
500: generic server; error 404: resource not found (operation or chain); 403: forbidden (not enough rights); 401: not authenticated (need to be logged); 409: conflict (dirty update)
This work also includes some changes on the PageProvider.
The topic of the next Nuxeo Tech Talk is AngularJS. We’ll welcome Julien Boucquillon who will talk about AngularJS in mobile apps. Damien Metzler will give an overview of the work we’ve done so far around AngularJS.
To answer that question, the first thing you need to know is that a tab is what we call an Action. An action can also be a link (a tab is basically a link styled as a tab) or a button, anything clickable in fact. And as stated in the documentation, you can filter theses actions. Take a look at the filter extension point in Nuxeo Explorer. As you can see there are several ways to filter actions.
Let’s say that the documents where we want to hide the notification tabs are all of a special document type. We could add a filter like this:
The action associated with this filter would not be displayed if the current document type was SpecialDocType. To associate this filter to an existing action, you need to override the action definition. To do that you need to know the id of the action you want to override. Most of the times if you look at the html code of the button or action you want to override, you’ll see the action id in the element id. You can also use the search in Nuxeo Explorer. Here, the id of the notification sub tab is TAB_MANAGE_SUBSCRIPTIONS. So to override the action, you have to do as follow:
And this is all you have to do. Now this is not exactly what was asked in the question. The filter needs to apply for all documents under a specific path. So you would have to use the condition filter as follow:
Make sure you don’t use an EL too heavy to evaluate. Filters are evaluated regularly so badly designed filters can have an impact on performance. If you want to know more about filters, make sure you read the appropriate documentation.
Image may be NSFW. Clik here to view.
Yesterday I was at DotJS with some Nuxeo folks because, you know, JavaScript is the new shit. So we need to stay up to date and we could also get some ideas for blog posts. It was quite interesting.
You were thrown off guard right at the beginning when Sylvain Zimmer, who was our MC for the day, asked us to introduce ourselves to our neighbors. They do this at every dot conference and it’s actually quite cool. It sets everybody in the right mood.
Addy Osmani had the difficult task to open the conference. He talked about JS tooling, Web Components, Polymer, about building big applications in HTML. One of the first things he showed was complex tags. Basically you hide complexity in a single HTML tag. Funny how it feels like JSF, only closer to the browser.
He also talked about the modern front end developer workflow, about the Grunt, Bower, Yeoman triptych. We started using it at Nuxeo and it does makes life easier. We actually plan to build a Yeoman generator for several basic JavaScript Nuxeo applications.
I think my favorite talk of the day was given by Guillermo Rauch. I strongly recommend that you go through his slidedeck, it’s already online. The title speaks for itself: The need for speed – Single-page apps. Optimistic UIs. Reactivity.
It’s about the benefits you’ll gain from building applications closer to the browser, using the browser as a platform. And it’s also about the perception of speed. Changing the layout of your page when the users starts an action will give an impression of speed. He took the Google landing page as an example. When you start typing something in the search box, the layout changes immediately, even if the search query is not over. This still gives an impression of speed, and you don’t have to display a lousy spinner or hourglass.. Seriously, go through his slidedeck, it’s great.
As a more general feeling, many speakers talked about the JavaScript tooling, to make JS easier to work with, easier to learn. Some went further and used languages that compile to JS. It feels like everybody wants to use the the browser as a platform, but are stuck with the native, cross-browser language that is JS. And everybody is trying to change that, whether by enhancing tooling, creating new web languages or enhancing JS virtual machines. Let’s just hope every browser vendor will find some common ground!
I am going to publish a series of short posts on how we use the Nuxeo Platform internally at Nuxeo. As you will see, there are indeed several use cases that are worth sharing. Eating our own dog food (as it is called) is very important in our product management process: it helps us understand what the limits of the platform are, it gives us the desire to continuously improve it, and it is also a great way to test the product in some more specific ways.
As you can imagine, maintaining a platform is far more complex than a simple out-of-the-box application, with a framework the test cases can be infinite. Thanks to this process, we are able to discover and fix a great number of bugs before releasing!
I’ll skip the basic document sharing use case as there are no surprises here: Nuxeo Platform is our intranet tool, and everybody uses it as well as the Drive extension for sharing documents internally. We also have more advanced use cases that I will share with you in this series:
Our vacation request workflow
The internal Techlead report publishing using the template rendering module
Nuxeo Connect and Marketplace applications – all Studio based
A data extraction chain made using Studio
Our customer reference management tool
A simple Q&A module
Our QA report plugin
A training / consulting workflow
Vacation Request Workflow
I’ll start today with the vacation request workflow – one of the oldest use cases for our platform. As in every company, people have to take vacations at Nuxeo (note the very corporate “have to” ;-). Asking for vacation at Nuxeo used to be an email-based process. This was working quite well, but was a nightmare for Ornella, our HR manager, when it came to checking what was taken for a give period of time.
As soon as we introduced Automation in 2010, we decided to implement this simple process on our intranet, a bare Nuxeo Platform. Since then, the Time Off Request module has been continually improved, following the evolution of Nuxeo Platform’s workflow capabilities. This included the use of the Tab Designer in 2011, Content Routing at the end of 2011, and the newer Content Routing features in 2013 (like escalation).
Then the request is submitted to the manager of the employee. Finally it goes to the HR Manager, who logs it into our internal ERP for the wages edition. It is possible to go back and forth at every step.
Users are notified via email, with details of the request included in the email. Some callbacks are sent when the manager processes the request late. It is also possible for the user to ask for a modification of an already validated request, as well as a cancellation.
A dashboard enables a user to see specific time-off-request process tasks, and to browse past requests. A specific ACL policy is applied so that a normal user always sees his/her own requests, whereas a manager can view the requests of his/her team, and the HR manager views all requests. Color codes are used at every step to show if the request has been accepted, is in the validation process, or was refused. The HR manager can filter by dates and user, and can do an Excel export of all the data, so as to be able to do some yearly aggregation, or other computation.
After 2-3 years of being used, we can say the application did reach its goals of improving the trace-ability of vacation requests, and definitely saved Ornella’s time, while not consuming too much of mine. Of course we followed all of the Nuxeo Platform releases and improved the plugin each time.
One improvement we would like to add in the future is to post in our internally shared Google time-off calendar the periods where an employee is absent. This should not be too hard to add, although it will require a bit of Java (there is currently no Java in the plugin, it is all done via Studio). Ornella recently asked for more workflows – with Nuxeo hiring many consultants and developers, streamlining this process is getting critical!
The project is shared as an application template in Studio (I just re-shared it as it was a very old version that was shared up until now). Since the implementation started even before content routing existed, the implementation state is limpid as if I was starting this project just now. There is a Time Off Request document object. I use the workflow to fetch the data from the user, and then persist it on the document in the output chains of the nodes of the workflow. I use the operation StartWorkflow to start the workflow immediately after creation of the time off request, without requiring the user launch it manually.
The Modification/Cancellation request is another workflow that can be started on the Time Off Request document type. I use the setACL operation with a specific “HR” ACL (Nuxeo Platform handles named ACLs) that I set after each node for controlling whether the time off request can be modified, and by who. For letting the system know who is the boss of who, I use a vocabulary. Ornella fills the correspondence table between username of the employee (in the id field of the vocabulary) and username of their manager (field “label” of the vocabulary). Then, in the workflow, on the node where a task is assigned to the manager, I “compute” the manager username in the input automation chain, using the following MVEL instruction:
We have just released a new version of Nuxeo IDE. It’s an Eclipse plugin that dramatically eases the development of Nuxeo modules. You can get it from our update site or from the Eclipse marketplace.
Code Completion
Among the new features available is XML completion for components and extension points. This has been developed by an external contributor, Sun Tan, that some of you may know since he used to work at Nuxeo and now works at Serli. He gave a lightning talk about his contribution during Nuxeo World. Take a look at the video. It’s a great feature. Thanks Sun!
As with most releases of Nuxeo IDE, we have added some new wizards. You can now generate a basic unit test skeleton, or Studio based unit test skeleton. This means that your Studio project will be deployed in your test setup, so you’ll be able to test it. The project’s POM will be updated accordingly.
The Marketplace package wizard also makes it to this new release. It lets you create a project that builds a marketplace package according to your deployment profile, and also generates some integration tests (Webdriver, Selenium, Funkload). It is only bound to one project for now but will be updated in the next release.
Reload Studio Projects from Nuxeo IDE
When you hit the server reload button, and one of the projects to redeploy is bound to a Studio project, it now checks the update date of the Studio project and will download and reload it if necessary.
Root POM Improvement
We’ve cleaned up the POM generation for Nuxeo IDE plugin projects. If you don’t fill the parent POM form, we generate a root POM with everything you need (maven plugins, repositories). And if you do choose a parent, the resulting POM will be much simpler.
Nuxeo IDE Operations to Studio Export
The Studio registry is no longer overwritten when exporting operations from an IDE project to Nuxeo Studio.
This time we invited Julien Boucquillon to talk about “AngularJS for Mobile” and our own Damien Metzler talked about AngularJS in the Nuxeo Platform.
If you follow this blog, you know that we have been working with AngularJS for a little while now, and are pretty happy about it. But so far we have not tried it in a mobile context. So it was really interesting to have someone give us some context and real-life tips about writing mobile apps with AngularJS.
Julien started by explaining how the two way binding works, how it’s not magical at all and how quick it can become costly, especially on mobile browsers. So he gave us some tips and tools to control this. Take a look at his slide deck on his website. It contains some nice code snippets, which makes it very real and comprehensive.
Then Damien explained what we do with AngularJS. He started by presenting quickly the Nuxeo Platform and how sometimes customers want to write a new application from scratch. This is why we’ve tried AngularJS. And we quickly realized that we needed a REST API to manipulate documents or users directly instead of using Content Automation. Then he went on to show a sample application with code snippets and finally explained what we want to do at Nuxeo in the near future with REST APIs.
Be sure to join our Meetup group in Paris to get notified about future Nuxeo Tech Talks.
For the next post in my “eat your own dog food” series, I want to talk about one of our uses of the nuxeo-template-rendering module for the internal “Techlead report”.
The Techlead report is an on going institution at Nuxeo that everyone reads to keep up to date on what is happening, from the pre-sales team to the CEO, to the developers and the support team. Laurent also publishes minutes of the report on this blog – you may have read it already.
Approximately once every two-three weeks, all the developers meet Thierry D. and Florent, CTO and Director of research and development respectively, to discuss what they are working on, both on the product implementation side and on the customer consulting side. During this meeting we gather all the technical challenges that the platform meets so as to prioritize the evolution – try to find new solutions to old problems, find some help on which angle to take on a new module implementation, and so on…
Thierry writes down the minutes of each discussion and every two-three weeks produces the “Techlead report.” As you can guess, it’s very long and the challenge was to get something “light” to read and easy to browse, while staying easy to author. Its format has evolved quite a bit in the past years: a simple mail, a Nuxeo “Blog Post”, a bare Note document type…. The last format, a Note document type, written in Markdown, with a website generated using the nuxeo-template-rendering module has been there for long enough to let us think we finally found the right recipe. :)
Using Nuxeo Drive
Thierry writes the global report content using his favorite text editor (no troll here, I won’t give the name) and it’s automatically synchronized as a Note document type in the intranet using Nuxeo Drive. Thanks to Nuxeo Drive, Thierry can work on the report at any time, anywhere and the content of the report is always synced in the intranet.
Markdown format support on Nuxeo Platform
Thierry writes the global report content using the Markdown syntax. This syntax is easy to read: you barely need a rendering engine to understand the “format” the author wanted to display. Of course, you still want bold, titles, subtitles, etc… in the end. :) Nuxeo supports the Markdown format on its Note document type, and can act as a rendering engine for Markdown syntax.
You can test it by creating a Note and choose “Markdown”, in the syntax field. Then type the following string: “
# This is a title
## This a subtitle
- and this is a bullet point
- and another *bullet point with some bold text*.
and save.
Image may be NSFW. Clik here to view.
You can see that Nuxeo automatically displays on the summary tab of the Note correctly formatted content. Yet, if the content is too long, the “summary tab” of Nuxeo is probably not the best option, plus it doesn’t allow you to browse among all the sections.
Another available option is to download it as a PDF, with correct formatting of the content (the markdown rendition is automatically integrated to the pdf conversion by the converter system of the platform). Still, being on the browser and then being redirected to another application (namely a PDF reader) is not 100% great in terms of flow.
Using the Nuxeo Template Rendering module
Thierry thought of using the nuxeo-template-rendering module to generate, on-the-fly, an autonomous website, based on the markdown Note content. The result is the following website, where users see an index of all the sections and subsections on the left, and can click on each subsection.
The first time Thierry creates the document in Nuxeo, he clicks on the “bind template” user action (on the top right bar of actions) and chooses the “Techlead report template”. The template had previously been created as a document under the Template folder, at the root of the domain.
The template document holds:
A main file, the freemarker template that will generate the website.
JavaScript/CSS resource files, used by the generated website.
Configuration properties.
A filter to set which Nuxeo document types it use.
The rendering engine to use, i.e. freemarker in this case (There are many others, like xdoc for renditions based on .doc files, jod for open office documents, jxl for Excel files, XSL/FOP – the platform allows you to contribute other rendition engines).
Variable replacement logic. The freemarker template below contains the variable @{htmlContent}. This variable must be referenced as a parameter on the Nuxeo Template. The nuxeo-template-rendering module handles several kinds of parameters: string, date, boolean, document property, image, inclusion. String, date and boolean include static values. Document propriety allows you to dynamically add the value of a property of a document. Image includes a picture, and inclusion is used for other kinds of content inclusion. For our use case, we use inclusion, and more specifically, HTML Preview – we include in the freemarker template the generated html preview (that will be tweaked by the JavaScript libraries that are added). Since there is a bug setting it on the template, the parameter is set specifically on the document, on the template parameters tab.
The “rendition” from which the result of the rendering engine is made accessible – Webview in our case.
As you may know, we just freed our first release of the iOS SDK to access our REST API. Before we blog about how to start a whole application with things like UIListView, we discuss the basics to get you started on something if you already are a UIKit compliant person.
Adding SDK to Any XCode Project
We recommend you use CocoaPods as your dependency management system to include our library. Do not hesitate to look at it deeper, it’s good, you can eat it. To sum it up, after installing CocoaPods:
Add Nuxeo pods specs repository:
$ pod repo add nuxeo https://github.com/nuxeo/cocoapods-specs
Create a text file named `Podfile` with this content:
platform :ios, '7.0'
pod 'NuxeoSDK', :head
Execute `pod` to build the workspace, then open it:
$ pod
$ open MyProject.xcworkspace
Requestinga document
Instead of talking a lot before coding, let’s start with something concrete:
You can initialize a session from the init method, or by using the shared one filled with NUXSession-info.plist content. Session can also set default behaviors for further requests, such as if you want to get the dublincore schema for all requests; add it with addDefaultSchemas method.
-> NUXSession documentation
In this sample, we initialize a session binded to a server hosted on “http://localhost:8080/nuxeo”, authenticated with default credentials and expecting to have the dublincore schema for all requests.
[2] Request
Requests are attached to a session, and let you access the REST API. You have access to some methods that hide URL construction, such as if you want to use an adaptor, to set a category. It also lets you set HTTP stuff like request headers, parameters, method, etc…
-> NUXRequest documentation
In this sample, we manually instantiate a request and add some URL segments to access the fetch document endpoint (/path/default-domain).
[3] Result blocks
We need to use blocks patterns, so we define two blocks: one when the request is successful, and the other if something goes wrong.
[4] Requests execution
You can start the request synchronously, or asynchronously, depending on what you need. Keep in mind to not block the UI thread.
-> NUXRequest documentation
In this sample, we forget to handle a failure case (I’m confident.) but the request is started asynchronously.
[5] Do something with the response
As soon as the remote server gives an answer, your block is called with the executed request filled with response data: responseStatusCode, responseMessage, response. You can access the response as bytes (if you love fighting with bytes), as an UTF8 encoded string, as a JSON dictionnary or as a NUXEntity.
-> Response handling documentation
-> NUXEntity mapping documentation
In this sample, we get the response as a NUXDocument which is a NUXEntity implementation for “document” entity-type response, and do “somethingGreatWith”. We skip over error result …
To Go Further
This is all for today. If you want to go further, you should take a look at the following links:
Image may be NSFW. Clik here to view.Nuxeo Platform 5.9.1 is out! It’s the first Fast Track release before our next LTS. Take a look at the release notes for the whole story. This new version does not contain many new user-visible features. Instead we focused on the infrastructure, doing some background work to enhance the platform and making it more accessible for developers.
One feature that you must take notice of and try is the Mule ESB connector. Thierry has been working on this for a while with Alain. They came up with a stable connector and a sample/documentation that will help you grasp the potential of this solution.
Another noticeable addition is the iOS SDK. It’s here to help you build iOS applications connected to the Nuxeo Platform through the REST API. It already has several cool features like the blob offline cache.
5.9.1 is also the first Nuxeo Platform release compatible with IE 11 thanks to Guillaume’s hard work on RichFaces.
As most of the changes are aimed at developers, don’t forget to read the upgrade notes. We have renamed a bunch of modules and packages so this might impact your code!
Image may be NSFW. Clik here to view.
The other day I started looking at TogetherJS. It’s a JavaScript library developed by Mozilla which enables collaboration between users browsing the same website. Take a look at the demo they have recorded, it’s really cool.
It uses WebSockets to connect to a server which will echo back messages to other connected browsers. This way you can chat with others and see what they do on a page. It synchronizes the different input fields of the page, tells you where other users go and if you want to follow them, etc… The default public server is hosted by Mozilla, which means they’ll see everything that goes through it. But you can host your own server if you want to.
It also uses WebRTC which enables P2P communications between browsers. Right now it only works on Firefox, Chrome and Opera. It allows TogetherJS to support audio communication, so you can start a conversation with users browsing the same website that you do, providing you invited them.
I’ve recorded a quick demo of TogetherJS in Nuxeo to give you an idea:
And if you want to try it yourself, it’s available on the Marketplace. You can take a look at the full code on Github.
In theory it’s really easy to add TogetherJS to any website. Actually all you have to do is add this code to your page:
This will load TogetherJS when you click on the ‘Start TogetherJS’ button. I said in theory because I had to do a couple of changes to it.
Any action you do on a page, like simply moving your mouse cursor, will trigger events that will be sent to the Hub. The Hub is the server hosted by Mozilla that relays all the messages to the other connected clients. This event contains the id of the DOM object where the event occurred. When the element cannot be identified by an id, the CSS selector is used (like ‘:nth-child(1)’). In Nuxeo, most of the DOM objects have an id also containing the colon character. This makes default TogetherJS behavior erratic to say the least, because it handles what’s after the first colon like a CSS selector. I had to fix this and package it, making the deployment a little more complicated.
Here’s what I did once I had my Nuxeo-compliant version of TogetherJS. First I took Nuxeo IDE and generated a new plugin project. There I created the src/main/resources/web/nuxeo.war/ folder. In this folder I put the togetherjs-min.js file and the associated togetherjs folder. This is what you get when building your own version. To make sure all of this gets deployed, I created a src/main/resources/OSGI-INF/deployment-fragment.xml file containing the following code:
Instead of adding a hard coded button like in the official example, I added an action. Notice that it’s in the FOOTER category so it will be available anywhere in Nuxeo.
Eventually I added the script to the footer template. I did not go with the Theme solution as usual because TogetherJS infers the base URL. So it would not have worked with a URL like “/nuxeo/nxthemes-lib/prototype.js,togetherjs.js….”.
Here’s what I added at the end of the src/main/resources/web/nuxeo.war/incl/nuxeo_footer_template.xhtml file:
And this is pretty much all I had to do. Now I can enjoy real-time collaboration and chat when editing a document on the intranet or simply browsing it.
Image may be NSFW. Clik here to view.You may have noticed our recent communication on the Mule ESB Connector for the Nuxeo Platform. We indeed did quite some work with implementing a fully workable connector for the Mule ESB platform. This connector gives you a way to leverage Nuxeo Platform Automation API (atomic operations) in the Mule ESB environment. Inside a Mule Flow, you can call Nuxeo Automation operations and transform the response payload in formats commonly accepted by other Mule ESB connectors. You can also set a “Nuxeo Listener” endpoint so that your Mule Flow is the reaction to an event happening in the Nuxeo Platform repository. Finally, we implemented the support of “DataSense,” which offers to Mule ESB users metadata autocompletion and automated introspection of payloads and schemas. There are dozens of connectors in the Mule library (http://www.mulesoft.org/connectors), meaning that this connector offers you the possibility to integrate your Nuxeo repository with other software platforms, such as Salesforce, Marketo, SAP, Magento, and many more.
There is a complete step by step tutorial to help you understand the basics of the Mule ESB Connector. In this blog post, I chose to show how to integrate the Nuxeo Platform with Twitter. The Mule Flow you will see in the following videos shows how to fetch some specific tweets and create notes in Nuxeo from the tweet content, setting up a sort of web intelligence agent. Instead of a verbose step-by-step written description, I decided to capture the process in three short videos:
A functional demo of the implemented flow (4:12),
A general technical explanation of the flow (5:23),
The live implementation (7 :03).
Do not hesitate to give feedback when you test this yourself!
Still coding with AngularJS and Nuxeo, today I will show you another AngularJS sample I made using the REST API. The goal here was to let Nuxeo users ask and answer questions, a bit like we do on answers.nuxeo.com.
It’s a simple application which makes it a good use case for CRUD operations. It was really quick and easy to do even though I am far from an expert in AngularJS and JavaScript (but I could really feel it becoming easier). Most of the new stuff I learned came from egghead.io – you should start there if you don’t know AngularJS.
Here are a number of screenshots. Lise helped me a lot on this to make it look good, using Semantic UI/.
Let’s focus on the question asking. Here’s the template used for the form:
<div class="active side ui segment">
<form name="myForm" class="ui form control-group">
<div class="field" ng-class="{error: myForm.name.$invalid}">
<h2>Ask a question</h2>
<label>Title</label>
<input type="text" name="title" placeholder="A question as clear and concise as possible" ng-model="doc.properties['dc:title']" required>
<span ng-show="myForm.name.$error.required" class="help-inline">
Required</span>
</div>
<div class="field" ng-class="{error: myForm.site.$invalid}">
<label>Description</label>
<textarea name="description" ng-model="doc.properties['dc:description']" placeholder=" A factual description so that everyone understands the question and issue and can respond correctly" required></textarea>
<span ng-show="myForm.site.$error.required" class="help-inline">
Required</span>
</div>
<div class="field">
<label>Communities</label>
<select multiple id="communities" class="ui input medium" ng-model="doc.properties['qs:community']">
<option ng-repeat="community in communities">{{community.title}}</option>
</select>
</div>
<div class="field">
<button ng-click="save()" ng-disabled="isClean() || myForm.$invalid"
class="ui blue submit button">Ask</button>
<a href="#/questions" class="btn">Cancel</a>
</div>
</form>
</div>
As you can see it’s simple, and using semantic UI makes it even easier to understand. The communities are actually the social workspaces available on my Nuxeo instance. You can see how they are fetched by looking at the controller:
The first part of the controller creates an empty doc document of type question, without any properties. Then the nxSearch service is used to fetch the list of communities. That’s all we need to display the ‘Ask a Question’ form. If the user clicks on the Ask button, we execute the $scope.save() method. It uses the nxSession service’s createDocument method. The first argument is the path of the parent document, the second argument is our doc object updated with the fields of our form.
As you can see, the createDocument is dead simple too. It’s an HTTP post of a JSON object. If you want to see all the other endpoints available for the REST API, I invite you to visit http://demo.nuxeo.com/nuxeo/api/v1/doc/ and try the API directly!
A big event occurred recently. Something huge I did not see coming. Some days ago, I woke up in the morning and guess what? The year had changed. Can you believe it? Gosh! Is time running at light-speed or what? It’s the New Year. So – Happy New Year to you all!
Now, let’s start my first blog of the year. It’s about events and event handlers.
Event Handlers are at the core of many Nuxeo applications, because they are a great place to manage and centralize business rules. Basically, an event handler (also called a listener) subscribes to an event, and when this event occurs, the handler is run. It is actually subtler than that, because the event handler also defines conditions for which it allows itself to be triggered. So, for example, an event handler could subscribe to the “document created” event, but will be executed only if the created document is a File (i.e. it will not run if it is a Folder, or a Picture).
And Nuxeo provides many many(1) events. From the Studio point of view, we have this:
I just wrote “many many” and we can only see four events here. Where are the others? Actually, they are in the scrollable list. Of course. As of today, with Studio 2.16, the full list is(2):
So, in Studio, you select the event(s), you setup the enablement, and then you pick-up a chain to be run when the event fires and the enablement conditions are met.
Now a little story about the reason why I am writing this blog. I had the following need: When “Empty document created” is fired on my Claim document, I want to pre-fill some values. But these default values depend on the parent document. So for example, say I have a claim:department field. If the Claim is created inside a Folder, then the department must be set to “Marketing”. In all other cases, the department must be set to “Management”.
The point is, in the context of the “Empty document created” event, the current document has no parent yet. I mean, from the automation chain, you cannot use something like @{Document.parent.type …etc…}.
Well. Technically, you can: After all, you can do whatever you want(3) can’t you? This is a good opportunity to see the awesome new error messages of 5.8 – look at the server log and check the description of the error. In case you want to save some time, I’m telling you: Document.parent is null, so Document.parent.type generates the hell in the log file.
How can I get the type of the parent in this context? After spending some time searching, I found myself asking for help from my colleagues. I received an answer because they are cool, patient, understanding and kind to me(4). The answer was: “Use the parentPath value of the event’s options”.
All I had to do then was find where these options were, and how I could use them. Well, quite simply: RTFM. It’s described in Understand Expression and Scripting Languages Used in Nuxeo. More precisely, in the Event Handlers part of the page. At this time, there was nothing about “parentPath”, “destinationRef” and “Availability of These Properties”: That’s also how we improve the documentation. We get in trouble, we clear the trouble, and we update the doc so other people will not feel the same pain.
Back to this story: All I had to do was to get the parent document using its path with the following expression:
Event.context.getProperty(“parentPath”)
My final chain triggered for the “Empty document created” event. For a Claim document it is…
Fetch > Context Document(s)
Push & Pop > Push Document
Fetch > Document value: @{Event.context.getProperty("parentPath")}
Execution Context > Set Context Variable name: parentIsFolder value: @{Document.type == “Folder”}
Push & Pop > Pop Document
Document > Update Property value: @{parentIsFolder ? “Marketing” : “Management”} xpah: claim:department save: no
…and it works like a charm. Important reminder: Make sure the “save” box is not checked. If you do save the document in the context of the Empty document created event, the server will gently throw an error.
Now, let’s push this a bit further. If you want to get a list of available properties for a given event, then a good trick is to use @{Event.context.getProperties().toString()}.
Note When the documentation states that an expression used in MVEL is an object, you can look at the JavaDoc of the object, to check what’s interesting. So for instance, here, you have an open JavaDoc page, so find the Event class. Then, you follow the EventContext link and you see this getProperties method.
For quick testing, create a new event handler. Select all and every possible event, activate it for File, and bind it to a chain that logs the result of this expression. Something like:
Save, update the server, play with a File and watch the server.log file: You will find all the events and all the available properties for each event that occurred. For example, after creating a new File, you will have:
Image may be NSFW. Clik here to view.I have been toying around a bit with Docker recently. It’s an open source project to easily create lightweight, portable, self-sufficient containers from any application. The same container that a developer builds and tests on a laptop can run at scale, in production, on VMs, bare metal, OpenStack clusters, public clouds and more. An open source project to pack, ship and run any application as a lightweight container.
If you haven’t tried Docker yet, I invite you to do so. They have a neat getting started page.
Why Docker?
There could be a lot of reasons to use it at Nuxeo. Here are a few I have been thinking about:
Jenkins on demand build slaves
Cloud deployment (obviously)
Remote/unified dev environments
On demand converters for Nuxeo
Hence the Part 1 in the title. I can’t tell you when this will end, but I can tell you there will be more :-)
Anyway, I started doing a Nuxeo image. The Docker way would be to have an image for each process (apache2, postgresql, nuxeoctl) but I wanted to start with an all-inclusive image. I thought it would be simpler. :-)
And it is indeed quite simple once you know your way around the supervision tool. Because when you run a Docker image, you can only run one process. So you have to use tools like Supervisor in order to run more processes. I have chosen Supervisor because it was the most widely used as far as I could see on the web. So my first working image contained everything needed to run Nuxeo and used Supervisor to launch everything. And everything was in a single Dockerfile.
A Dockerfile is like a recipe to build an image. It contains a list of steps that are all versioned. Differences between each step of the image are stored on the filesystem. That means that if you add a new step at the end, for instance, you don’t have to go through all the steps again.
Putting every step in a single file is of course not the best way to do it when you want to do different Nuxeo images. For continuous integration, for example, I will need an image with H2, one with PostgreSQL, one with MySQL etc… So I started splitting my Dockerfile to have something more modular. Here’s the result.
The Base Image
The purpose of this image it to give you an up to date Ubuntu distribution with all the dependencies needed by Nuxeo, a nuxeo user and Supervisor to manage your processes. This will be the basis for the next images to build.
# Nuxeo Base image is a ubuntu precise image with all the dependencies needed by Nuxeo Platform
#
# VERSION 0.0.1
FROM ubuntu:precise
MAINTAINER Laurent Doguin <ldoguin@nuxeo.com>
# Set locale
RUN locale-gen --no-purge en_US.UTF-8
ENV LC_ALL en_US.UTF-8
# Install dependencies
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get install -y python-software-properties wget sudo net-tools
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
# Add Nuxeo Repository
RUN apt-add-repository "deb http://apt.nuxeo.org/ precise fasttracks"
RUN wget -q -O - http://apt.nuxeo.org/nuxeo.key | apt-key add -
# Upgrade Ubuntu
RUN apt-get update
RUN apt-get upgrade -y
# Small trick to Install fuse(libreoffice dependency) because of container permission issue.
RUN apt-get -y install fuse || true
RUN rm -rf /var/lib/dpkg/info/fuse.postinst
RUN apt-get -y install fuse
# Install Nuxeo Dependencies
RUN sudo apt-get install -y acpid openjdk-7-jdk libreoffice imagemagick poppler-utils ffmpeg ffmpeg2theora ufraw libwpd-tools perl locales pwgen dialog supervisor unzip vim
RUN mkdir -p /var/log/supervisor
# create Nuxeo user
RUN useradd -m -d /home/nuxeo -p nuxeo nuxeo && adduser nuxeo sudo && chsh -s /bin/bash nuxeo
ENV NUXEO_USER nuxeo
To build this image, get into the folder containing the Dockerfile and run:
docker build -t nuxeo/nuxeobase .
It’s always good to use the -t option and give a name to the images you build. It’s even more important since it will be used in the next image.
Note that running this image won’t get you anywhere, there is no Nuxeo installed on it.
An All Inclusive Nuxeo Image
This Docker image is based on the work Mathieu did for our VM. I had to adapt one or two things but it’s really close to the original. I mostly added supervisor since I have this one process limitation. Here’s the configuration I used:
It will run Apache, PostgreSQL and the ssh daemon. But as you can see there is still no trace of a Nuxeo process. It will actually be launched by the pgListener event listener. It’s a python script that waits for the postgresql process to be running. The code is pretty simple as you can see. You just have to use the Supervisor Python API to read Supervisor events. Once the event saying the postgresql process entered the running state occurs, the firstboot.sh script is launched.
#!/usr/bin/env python
import os
import sys
from supervisor import childutils
def main():
while 1:
headers, payload = childutils.listener.wait()
if headers['eventname'].startswith('PROCESS_STATE_RUNNING'):
pheaders, pdata = childutils.eventdata(payload+'\n')
if pheaders['processname'] == "postgresql":
os.system("sh /root/firstboot.sh")
break
childutils.listener.ok()
if __name__ == '__main__':
main()
Now about the firstboot.sh script. It configures the database if it has not been done already and starts Nuxeo.
#!/bin/bash
# Prevent firstboot from being executed twice
if [ -f /root/firstboot_done ]; then
su $NUXEO_USER -m -c "$NUXEOCTL --quiet restart"
exit 0
fi
# PostgreSQL setup
pgpass=$(pwgen -c1)
su postgres -c "psql -p 5432 template1 --quiet -t -f-" << EOF > /dev/null
CREATE USER nuxeo WITH PASSWORD '$pgpass';
CREATE LANGUAGE plpgsql;
CREATE FUNCTION pg_catalog.text(integer) RETURNS text STRICT IMMUTABLE LANGUAGE SQL AS 'SELECT textin(int4out(\$1));';
CREATE CAST (integer AS text) WITH FUNCTION pg_catalog.text(integer) AS IMPLICIT;
COMMENT ON FUNCTION pg_catalog.text(integer) IS 'convert integer to text';
CREATE FUNCTION pg_catalog.text(bigint) RETURNS text STRICT IMMUTABLE LANGUAGE SQL AS 'SELECT textin(int8out(\$1));';
CREATE CAST (bigint AS text) WITH FUNCTION pg_catalog.text(bigint) AS IMPLICIT;
COMMENT ON FUNCTION pg_catalog.text(bigint) IS 'convert bigint to text';
EOF
su postgres -c "createdb -p 5432 -O nuxeo -E UTF-8 nuxeo"
# Nuxeo setup
cat << EOF >> /etc/nuxeo/nuxeo.conf
nuxeo.templates=postgresql
nuxeo.db.host=localhost
nuxeo.db.port=5432
nuxeo.db.name=nuxeo
nuxeo.db.user=nuxeo
nuxeo.db.password=$pgpass
EOF
su $NUXEO_USER -m -c "$NUXEOCTL --quiet restart"
# Prevent firstboot from being executed twice
touch /root/firstboot_done
Now about the Dockerfile. Notice the first line that says FROM nuxeo/nuxeobase. It’s the name of the image built earlier. Then what happens is we download the latest LTS, set up environment variables, install PostgreSQL, Apache and the OpenSSH server. Once this is done we add different configuration files and scripts. Supervisor.conf and nuxeo.apache2 are respectively the Supervisor configuration and the Apache configuration. I have already told you about pgListener.py and firstboot.sh. What about the two others?
Postinst.sh is dedicated to configuration. We use it to extract Nuxeo from its archive, create the various files and folders needed (like /etc/nuxeo/, /var/lib/nuxeo, etc..), setup the good permissions, modify nuxeo.conf, drop the main PostgreSQL cluster and create our own nuxeodb cluster and setup Apache.
Entrypoint.sh is run each time you start the Docker container thanks to the ENTRYPOINT command. I use it to re-generate the ssh keys and set a random password if it has not already been done. Then I echo the password and run what’s been given as CMD using exec “$@”
I am not sure if it’s supposed to be used like that, but it allows me to display the password and keep flexibility through CMD, because if you don’t want to run supervisiond directly, you can give /bin/bash at the end of your docker run command.
# Nuxeo Platform
#
# VERSION 0.0.1
FROM nuxeo/nuxeobase
MAINTAINER Laurent Doguin <ldoguin@nuxeo.com>
# Download latest LTS nuxeo version
RUN wget http://community.nuxeo.com/static/releases/nuxeo-5.8/nuxeo-cap-5.8-tomcat.zip && mv nuxeo-cap-5.8-tomcat.zip nuxeo-distribution.zip
ENV NUXEOCTL /var/lib/nuxeo/server/bin/nuxeoctl
ENV NUXEO_CONF /etc/nuxeo/nuxeo.conf
# Add postgresql Repository
RUN apt-add-repository "deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main"
RUN wget -q -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
# Install apache and ssh server
RUN sudo apt-get install -y openssh-server apache2 postgresql-9.3
RUN mkdir -p /var/run/sshd
ADD supervisord.conf /etc/supervisor/conf.d/supervisord.conf
ADD nuxeo.apache2 /etc/apache2/sites-available/nuxeo
ADD postinst.sh /root/postinst.sh
ADD firstboot.sh /root/firstboot.sh
ADD entrypoint.sh /entrypoint.sh
ADD pgListener.py pgListener.py
RUN /root/postinst.sh
EXPOSE 22 80
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/usr/bin/supervisord"]
You can run it like this:
docker run -P -d nuxeo/nuxeo
Now your container should be running, let’s see if this is true. Typing docker ps should output something like this:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
02717b82d503 nuxeo/nuxeo:latest /entrypoint.sh /usr/ 5 seconds ago Up 4 seconds 0.0.0.0:49153->22/tcp, 0.0.0.0:49154->80/tcp sick_tesla
Thanks to the -P option used in the run command, all the exposed ports are mapped automatically. So if you go to yourDockerHost:49154 with a web browser, you’ll have the running Nuxeo instance.
Now for some reason, like say debugging, you might want to open an ssh session to the server. We already have the port thanks to docker ps. Now we need the password. It’s in the logs thanks to the entrypoint.sh script, so if you type docker logs 02717b82d503, 02717b82d503 being the container id, you should see something like:
Creating SSH2 RSA key; this may take some time ...
Creating SSH2 DSA key; this may take some time ...
Creating SSH2 ECDSA key; this may take some time ...
start: Unable to connect to Upstart: Failed to connect to socket /com/ubuntu/upstart: Connection refused
Default password for the root and nuxeo users: EzeegaiN
2014-01-16 16:11:52,410 CRIT Supervisor running as root (no user in config file)
2014-01-16 16:11:52,410 WARN Included extra file "/etc/supervisor/conf.d/supervisord.conf" during parsing
2014-01-16 16:11:52,436 INFO RPC interface 'supervisor' initialized
2014-01-16 16:11:52,436 WARN cElementTree not installed, using slower XML parser for XML-RPC
2014-01-16 16:11:52,437 CRIT Server 'unix_http_server' running without any HTTP authentication checking
2014-01-16 16:11:52,437 INFO supervisord started with pid 1
2014-01-16 16:11:53,439 INFO spawned: 'pgListener' with pid 70
2014-01-16 16:11:53,441 INFO spawned: 'sshd' with pid 71
2014-01-16 16:11:53,443 INFO spawned: 'postgresql' with pid 72
2014-01-16 16:11:53,445 INFO spawned: 'apache2' with pid 73
2014-01-16 16:11:54,737 INFO success: pgListener entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2014-01-16 16:11:54,738 INFO success: sshd entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2014-01-16 16:11:54,739 INFO success: postgresql entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2014-01-16 16:11:54,739 INFO success: apache2 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2014-01-16 16:12:05,617 INFO exited: pgListener (exit status 0; expected)
The password is on the fifth line. So now you can do something like ssh nuxeo@yourDockerHost -p 49153 and open a session to your container.
And there you go now you have a fully functional docker container to test Nuxeo.
Image may be NSFW. Clik here to view. This blog post was written by Caleb Burch, Product Engineer at Ikanow. Ikanow is a software organization that has created the world’s first open source analytics platform.
This blog will demonstrate how we connected the Infinit.e platform’s sign on with Nuxeo’s to allow a seamless interaction (single sign on) for our clients using Nuxeo’s case management capabilities. Infinit.e is an open source analytic platform designed for documents not database records of which it provides a wide range of activities including search, visualization, and data science. Our customers use the platform for financial fraud detection, cyber security, geopolitical analysis, marketing, and other fields involving large sets of unstructured data. We use Nuxeo as a case manager to allow users to map abstract concepts like data sources, query sets, statistical recommendations, and important entities back to “real life” artifacts like suspects, leads, or evidence. The need to communicate with Infinit.e and manage user/group permissions meant we had to develop a single-sign on capability between the platforms.
The typical interaction between the two platforms usually involves mapping some entity discovered in a document to our Case Visualizer (pictured below). This is a lot like a working canvas for analysis so users can store data while they work between queries, exploring data. Once the user decides an entity is important, the entity can be promoted to a target.
When a target is created, we auto create Nuxeo-documents (pictured below) for these targets so a user can manage them more easily. We also have some more integrated actions that allow users to kick off Infinit.e data collection based on the targets in Nuxeo. Image may be NSFW. Clik here to view.
Based on these typical user interactions, the goals of connecting our 2 platforms were to:
Allow users to automatically move from our web application into Nuxeo without having log in twice
Transfer permissions from the Infinit.e community system directly to Nuxeo groups (they are similar authentication schemes of grouping users into permission groups)
Create a custom Authenticator that implements NuxeoAuthenticationPlugin, NuxeoAuthenticationPluginLogoutExtension (This authenticator will check we have an active connection with Infinit.e, get our user groups, and log us out)
Create a custom Login Plugin that implements LoginPlugin
Create a custom configuration file that is external to our extensions so we can adjust it for different Nuxeo instances.
Adjust the default authentication chain order for both default login and automation
Deploy to your Nuxeo server
Infinit.e architecture
Infinit.e is a web application powered by a RESTful API service. We used three of our API calls to authenticate users and get their communities (groups).
We authenticate login via the Auth – Login. This is a restful call in which we return a cookie named “infinitecookie” that will be active for 15 minutes from the most recent API call (e.g. if you keep making subsequent API calls, the timer will be updated for 15 more minutes).
Step 1: Create a custom Authenticator that implements NuxeoAuthenticationPlugin, NuxeoAuthenticationPluginLogoutExtension – this authenticator will check we have an active connection with Infinit.e, get our user groups, and log us out
We first needed to create our own Authenticator that will check if a user is logged into Infinit.e already, grab the user object, and create a user/groups in Nuxeo as necessary. We accomplished this by creating a new Nuxeo plugin and creating a class that implements both NuxeoAuthenticationPlugin and NuxeoAuthenticationPluginLogoutExtension, thus overriding those extension points.
The methods we needed to override included:
handleRetrieveIdentity
This is the meat of the Authenticator. Here we check if a user has a valid cookie in the httpRequest, then we grab an Infinit.e user object with that cookie and create a Nuxeo user if one does not exist, as well as any Nuxeo groups that do not exist. After we create all that, a UserIdentificationInfo object is returned for that user. This object will be sent off to the Login Plugin to validate the account is valid, but we are going to over ride that to make sure it always thinks it is valid so we can just put whatever password we like.
NOTE: On as user’s first call to Nuxeo you may not yet have an active jsp session, so we must call httpRequest.getSession(true); to make sure one is created.
initPlugin
This method gets called when Nuxeo starts up, allows us to grab config params from the configuration file we are going to create later. (e.g. we get the Infinit.e API url and the Infinit.e login url so we can redirect unauthenticated users)
needLoginPrompt
Checks if an httprequest needs to be sent to handleLoginPrompt, we always return true
handleLoginPrompt
Here we check if a user is authenticated, if not we redirect them to the login url (we received from initPlugin). Return false if there is nothing to do
getUnAuthenticatedURLPrefix
Block any urls you don’t want to allow access, we just return null here
handleLogout
Occurs when someone pushes the logout button in Nuxeo, we send a request to log the user out of Infinit.e and redirect them to the Infinit.e login page.
>InfiniteAuthenticator.java
package com.ikanow.infinit.e.nuxeo.auth;
import java.net.URLEncoder;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.NuxeoPrincipal;
import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo;
import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin;
import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPluginLogoutExtension;
import org.nuxeo.ecm.platform.usermanager.UserManager;
import org.nuxeo.runtime.api.Framework;
import com.ikanow.infinit.e.data_model.api.ResponsePojo.ResponseObject;
import com.ikanow.infinit.e.data_model.driver.InfiniteDriver;
import com.ikanow.infinit.e.data_model.store.social.person.PersonCommunityPojo;
import com.ikanow.infinit.e.data_model.store.social.person.PersonPojo;
public class InfiniteAuthenticator implements NuxeoAuthenticationPlugin, NuxeoAuthenticationPluginLogoutExtension {
private static final Log log = LogFactory.getLog(InfiniteAuthenticator.class);
private String infinite_api_url = "";
private String infinite_login_url = "";
private final String INFINITE_COOKIE = "infinitecookie";
@Override
public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
Cookie cookie = getCookie(httpRequest, INFINITE_COOKIE);
PersonPojo user = getInfiniteUser(httpRequest, cookie);
if (user != null) {
String user_email = user.getEmail();
//try to get user, or create one if they don't exist
try {
getOrCreateUser(user_email, user.getCommunities());
} catch (Exception ex) {
log.error("Error get/create user", ex);
return null;
}
httpRequest.getSession(true); //create a session if one does not exist, was having some issues w/ sessions breaking
UserIdentificationInfo uii = new UserIdentificationInfo(user_email, ""); //NO PASSWORD NECESSARY
return uii;
} else {
log.debug("Infinit.e Person was null");
return null;
}
}
private void getOrCreateUser(String username, List & lt; PersonCommunityPojo & gt; communities) throws Exception {
UserManager userManager = Framework.getService(UserManager.class);
//set up groups:
String[] groups = new String[communities.size() + 1];
groups[0] = "members"; //add default group
for (int i = 0; i & lt; communities.size(); i++) {
PersonCommunityPojo community = communities.get(i);
//add group to this users list
groups[i + 1] = community.getName();
//make sure group exists
DocumentModel group_doc_model = userManager.getGroupModel(community.getName());
if (group_doc_model == null) {
log.debug("Group: " + community.getName() + " did not exist, creating and adding this person as user.");
//create it
group_doc_model = userManager.getBareGroupModel();
group_doc_model.setProperty("group", "grouplabel", community.getName());
group_doc_model.setProperty("group", "groupname", community.getName());
group_doc_model.setProperty("group", "description", community.getName());
String[] members = new String[1];
members[0] = username;
group_doc_model.setProperty("group", "members", members);
userManager.createGroup(group_doc_model);
} else {
//make sure this person is a member and update group
@
SuppressWarnings("unchecked")
List & lt;
String & gt;
member_array = (List & lt; String & gt;) group_doc_model.getProperty("group", "members");
Set & lt;
String & gt;
members = new HashSet & lt;
String & gt;
(member_array);
members.add(username);
group_doc_model.setProperty("group", "members", members.toArray());
userManager.updateGroup(group_doc_model);
}
}
NuxeoPrincipal principal = userManager.getPrincipal(username);
if (principal != null) {
DocumentModel user_doc_model = userManager.getUserModel(username);
user_doc_model.setProperty("user", "groups", groups);
userManager.updateUser(user_doc_model);
} else {
log.debug("principal was null, create a new user");
DocumentModel user_doc_model = userManager.getBareUserModel();
user_doc_model.setProperty("user", "username", username);
user_doc_model.setProperty("user", "email", username);
user_doc_model.setProperty("user", "password", "fakepassword" + new Random().nextInt());
user_doc_model.setProperty("user", "groups", groups);
userManager.createUser(user_doc_model);
}
}
@Override
public void initPlugin(Map & lt; String, String & gt; parameters) {
log.info("init Infinite Authenticator");
if (parameters.containsKey("infiniteAPIURL") & amp; & amp; parameters.containsKey("infiniteLoginURL")) {
log.info("API_URL_PARAM: " + parameters.get("infiniteAPIURL"));
log.info("API_LOGIN_PARAM: " + parameters.get("infiniteLoginURL"));
infinite_api_url = parameters.get("infiniteAPIURL");
infinite_login_url = parameters.get("infiniteLoginURL");
}
log.debug("end init");
}
@Override
public Boolean needLoginPrompt(HttpServletRequest httpRequest) {
return true;
}
@Override
public Boolean handleLoginPrompt(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String baseURL) {
Cookie cookie = getCookie(httpRequest, "infinitecookie");
Boolean keepalive = isLoggedIn(httpRequest, cookie);
log.debug("keepalive success: " + keepalive);
if (!keepalive) {
try {
String redirect_url = httpRequest.getRequestURL().toString();
String security_header = httpRequest.getHeader("X-Forwarded-Proto");
if (security_header != null & amp; & amp; security_header.toLowerCase().equals("https")) {
redirect_url = redirect_url.replace("http://", "https://");
}
redirect_url = URLEncoder.encode(redirect_url, "UTF-8");
httpResponse.sendRedirect(getLoginUrl(httpRequest, infinite_login_url) + "?redirect=" + redirect_url);
return true;
} catch (Exception ex) {
log.error("unable to redirect", ex);
}
}
return false;
}
@Override
public List & lt;
String & gt;
getUnAuthenticatedURLPrefix() {
log.debug("In unauth url prefix: there are no urls we deny access");
return null;
}
private Boolean isLoggedIn(HttpServletRequest httpRequest, Cookie cookie) {
if (cookie != null) {
InfiniteDriver inf_driver = new InfiniteDriver(getApiUrl(httpRequest, infinite_api_url));
inf_driver.useExistingCookie(cookie.getValue());
return inf_driver.sendKeepalive();
}
return false;
}
private PersonPojo getInfiniteUser(HttpServletRequest httpRequest, Cookie cookie) {
if (cookie != null) {
InfiniteDriver inf_driver = new InfiniteDriver(getApiUrl(httpRequest, infinite_api_url));
inf_driver.useExistingCookie(cookie.getValue());
ResponseObject ro = new ResponseObject();
return inf_driver.getPerson(null, ro);
}
return null;
}
@Override
public Boolean handleLogout(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
//try to invalidate infinite cookie
Cookie cookie = getCookie(httpRequest, "infinitecookie");
if (cookie != null) {
InfiniteDriver inf_driver = new InfiniteDriver(getApiUrl(httpRequest, infinite_api_url));
inf_driver.useExistingCookie(cookie.getValue());
inf_driver.logout();
}
try {
//redirect to infinit.e login
httpResponse.sendRedirect(getLoginUrl(httpRequest, infinite_login_url));
return true;
} catch (Exception ex) {
log.error("unable to redirect", ex);
}
return false;
}
private String getApiUrl(HttpServletRequest httpRequest, String property) {
if (property.equals("AUTOMATIC")) {
try {
return "http://" + httpRequest.getServerName() + "/api/";
} catch (Exception ex) {
log.debug("error converting to url");
}
}
return property;
}
private String getLoginUrl(HttpServletRequest httpRequest, String property) {
if (property.equals("AUTOMATIC")) {
try {
return "http://" + httpRequest.getServerName();
} catch (Exception ex) {
log.debug("error converting to url");
}
}
return property;
}
private static Cookie getCookie(HttpServletRequest httpRequest, String cookieName) {
log.debug("trying to get cookie: " + cookieName);
Cookie cookies[] = httpRequest.getCookies();
if (cookies != null) {
for (Cookie cookie: cookies) {
if (cookie.getName().equals(cookieName)) {
return cookie;
}
}
}
return null;
}
}
Step 2: Create a custom Login Plugin that implements LoginPlugin
Next we needed to override the Nuxeo default Login Plugin because the default login plugin TrustingLM will not authenticate our users correctly (I think this is due to our using of random passwords but I am not positive)
The login plugin is a new class that implements LoginPlugin and overrides all the methods. It is very basic and we just return the username anytime it comes to validateUserIdentity
>InfiniteLoginPlugin.java
package com.ikanow.infinit.e.nuxeo.auth;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo;
import org.nuxeo.ecm.platform.login.LoginPlugin;
public class InfiniteLoginPlugin implements LoginPlugin {
private static final Log log = LogFactory.getLog(InfiniteLoginPlugin.class);
private String name = "InfiniteLoginPlugin";
private Map & lt;
String, String & gt;
params = null;
@Override
public String validatedUserIdentity(UserIdentificationInfo userIdent) {
return userIdent.getUserName();
}
@Override
public Boolean initLoginModule() {
return true;
}
@Override
public Map & lt;
String, String & gt;
getParameters() {
return params;
}
@Override
public void setParameters(Map & lt; String, String & gt; parameters) {
params = parameters;
}
@Override
public String getParameter(String parameterName) {
return params.get(parameterName);
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String pluginName) {
name = pluginName;
}
}
Step 3: Create a custom configuration file that is external to our extensions so we can adjust it for different Nuxeo instances
We could have put the authenticator properties in the extension contribution outlined in the next step, but we wanted the ability to adjust them for different server configurations so we moved some of the pieces outside (namely the API and Login urls). According to Nuxeo, this file’s name must end in -config.xml and be placed in NUXEO_INSTALL/nxserver/config/
The important thing to add here are any parameters you want – you can add and give whatever names/values you wish. They will be available in the Authenticator during the initPlugin function. We also let Nuxeo know the name of our Authenticator (INFINITE_AUTH), and the Login Plugin we wish to use (InfiniteLoginPlugin, the piece we made in Step 2).
Step 4: Adjust the default authentication chain order for both default login and automation
Next we needed to actually tell Nuxeo what extension point we were going to override, and change the order of the Authenticators to use ours first. To do this we needed to edit the config file that should have been automatically created in your plugin project at /src/main/resources/OSGI-INF/extensions/filename.xml
In the file:
We first defined our login plugin and give it a name (the same one you used above in step 3 in the config.xml, e.g. InfiniteLoginPlugin)
Next we overrode the authentication change, we tell Nuxeo to use our new InfiniteAuthenticator first for our platform, then resort to Form Auth if necessary (the only way to get to form Auth is via a direct link to nuxeo/login.jsp because we always redirect invalid logins back to Infinit.e).
Lastly we connected to Nuxeo using the automation client. We needed to override that specfic chain as well, calling InfiniteAuthenticator first, as you can see in the last extension.
NOTE: You may have some code in a .xml file for your Login Plugin, I recommend deleting it all out and putting it in this one configuration file. See example below for how we left the LoginPlugin.xml
>InfiniteAuthenticator.xml
<?xml version="1.0″?>
<component name="com.ikanow.infinit.e.nuxeo.auth.InfiniteAuthenticator">
<require>org.nuxeo.ecm.platform.ui.web.auth.defaultConfig</require>
<!– We need to use the Infinit.e login plugin because trusting lm is doing something weird and failing to authenticate users –>
<extension target="org.nuxeo.ecm.platform.login.LoginPluginRegistry" point="plugin">
<LoginPlugin name="InfiniteLoginPlugin" class="com.ikanow.infinit.e.nuxeo.auth.InfiniteLoginPlugin">
<enabled>true</enabled>
</LoginPlugin>
</extension>
<extension target="org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService" point="chain">
<authenticationChain>
<plugins>
<plugin>INFINITE_AUTH</plugin>
<plugin>FORM_AUTH</plugin>
</plugins>
</authenticationChain>
</extension>
<extension target="org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService" point="specificChains">
<specificAuthenticationChain name="Automation">
<urlPatterns>
<url>(.*)/automation.*</url>
</urlPatterns>
<replacementChain>
<plugin>INFINITE_AUTH</plugin>
<plugin>AUTOMATION_BASIC_AUTH</plugin>
</replacementChain>
</specificAuthenticationChain>
</extension>
</component>
<?xml version="1.0″?>
<component name="com.ikanow.infinit.e.nuxeo.auth.InfiniteLoginPlugin">
</component>
Step 5: Deploy to Nuxeo
Finally now that we’ve programmed all this up, we deployed it to Nuxeo to test using the following steps:
Build project by right clicking on project name > Nuxeo > Export Jar
Place this jar in your nuxeo instance at NUXEO_INSTALL/nxserver/plugins/
Place the -config.xml file we created in step 3 at NUXEO_INSTALL/nxserver/config/
Restart Nuxeo
After that point you should be able to login to an external service, then attempt to go to Nuxeo and it will automatically create a user account, groups and let you in.
Conclusion
The Nuxeo platform offers a flexible API and multiple extension points that allowed us to tightly integrate our log in infrastructure and data models with their authentication system. Nuxeo actually offers a few standard solutions to allow SSO such as CAS, Portal Authentication, and Token Authentication. We decided to build our own because we wanted to pass on Infinit.e’s user groups permissions without having to manage Nuxeo groups in addition to Infinit.e’s as well as be able to perform some automatic actions as users (create documents). Ultimately, capability allows us to build an improved user experience for our users taking advantage of Nuxeo’s case management capabilities.