xPages and Java : Usage of 3th party packages and jsonsimple.

A small post from my side about 3th party packages. Be sure whenever you make us of an 3th party jar file:

  1. Check the license file
  2. Make use of libraries which are open source when possible.
  3. Make sure the libraries don’t have lots and lots of dependencies ( other 3th party libraries )

One example to think about is the creation of JSON. When generating JSON in xPages you can make use of the IBM package:  com.ibm.commons.util.io.json.JsonJavaFactory. In the context of xPages this seems a logical step. But what happens if you want to reuse created classes in xPages in other projects for instance a native desktop app?

You could face the problem that the package is not available or not compatible with the jvm you are using but your custom classes are. In the worst case scenario you need to rewrite your code because you are adding another library. Therefore you will end up with 2 versions of the same code and as we all know.. that could be a pain in the .. to maintain.

In this case you could make use of the org.json.simple package. This package is opensource and makes use of the apache licens. So go fetch a copy of this great json library and start working with it!

ViewPanel vs. Dijit.Tree part 1

In this article I will describe how I converted this:

This

This in This
viewpanel grid

note: for these series you will need a copy of the fakenames.nsf file.

As we all know the default ViewPanel control is good for generic cases in where you want to translate the default notes charactaristics into a web application. One of the ‘features’ notes views offer are categories. And there is also the problem with ViewPanels. The HTML that is being produced by a viewPanel…  It ain’t highly customizable and sometimes just a pain in the ##!@ to style. And ofcourse there is also the great feature that categories that are displayed in the last row of the page don’t show their children on the same page as the category when expanded!

Luckily for us (some people think otherwise) xPages is shipped with Dojo. This javascript framework offers lots and lots of default UI widgets ready to use. One problem tho. You need to know how to adapt them in xPages and the notes structure. One of them is the dijit.tree. A dijit.tree as the same already suggests renderes a Tree of categories and childnodes. Just like the ViewPanel but then highly customizable and and fairly easy to generate. I tried to implement a dijit.tree using notesview data from the Fakenames.nsf which contains 40.000 person documents and succeeded in less then 5 hours of code crunching. These series will be about this proces. So lets start.

First of all we need to know how we can create a dijit.tree by example. To do this you need to copy the following code in a xPage.

<script type="text/javascript">

// First we import the correct dojo / dijit packages
    dojo.require("dojo.data.ItemFileReadStore");
    dojo.require("dijit.Tree");

// Second we define an function which will be run after the page has been loaded

    dojo.addOnLoad(function() {

// Create a data store to retrieve data from
// in this example we use the countries.json
        var store = new dojo.data.ItemFileReadStore({
            url: "countries.json"
        });

// secondly we create a treeModel. A treemodel are the classes / datastructure 
// on which the tree is running
        var treeModel = new dijit.tree.ForestStoreModel({
            store: store,
            query: {
                "type": "continent"
            },
            rootId: "root",
            rootLabel: "Continents",
            childrenAttrs: ["children"]
        });

// Last but not least we create a new instance of our tree. 
        new dijit.Tree({
            model: treeModel
        },
        "treeOne");
    });
</script>

<!-- The domnode we will use to render the tree -->
<div id="treeOne"/>

Next step is to add the following file countries.json to your file resources in the nsf as countries.json. If you fire up your newly created xPage you will see that a dijit.tree has been created

grid

As you can see with only a few lines of code we have created a great looking dijit tree. But how do we hook it up to an existing notesview? First of lets find out what the code does we just pasted. We will skip the dojo module loading ( dojo.require)

line 13 ~ 15:

A tree needs data to generate its tree from. On these lines we create a new variable in which we hold the filestore. This store is pointing to our countries.json file. But ofcourse this could also be an url / or a rest service! (hint)

19 ~ 27:

A dijit.Tree uses a treemodel to represents the actual data in the store. By adding / remove properties from the model we can influence how the data is being loaded and displayed. First we tell the model where to look for some data. In our case the local variabel store.

next we have to define the query which will be performed on the data. A query is nothing more then a statement that tells the model where to start expanding. Next are the rootId and rootLabel. These tell the model where to start and how to display the very first node in the data.

Last but not least the childrenattrs tells the model where to look for child nodes in the store.

30 ~ 33:

Here we really create the dijit.tree. In this object we specify on which dom node we want to  treemodel to be generated.

Easy. Isn’t it. Now lets have a look at the data that is stored in the countries.json and compare it with json that is generated by a notesview by default.

items: [
	        { id: 'AF', name:'Africa', type:'continent', population:'900 million', area: '30,221,532 sq km',
	        		timezone: '-1 UTC to +4 UTC',
	        		children:[{ id: 'EG', name:'Egypt', type:'country' },
	        	{ id: 'KE', name:'Kenya', type:'country',
	        			children:[{_reference:'Nairobi'}, {_reference:'Mombasa'}] },
	        		{ id: 'Nairobi', name:'Nairobi', type:'city' },
	        		{ id: 'Mombasa', name:'Mombasa', type:'city' },
	        	{ id: 'SD', name:'Sudan', type:'country',
	        			children:{_reference:'Khartoum'} },
	        		{ id: 'Khartoum', name:'Khartoum', type:'city' }] }
]

As you can see JSON contains an array called ‘items’. Which contains a node with id, name, type etc as its properties. Every node contains a Children array if there are any children. Very straightforward and easy to read.

Now lets take a look at the JSON notes gives us when we open a random view

{
"@timestamp": "20130610T205348,20Z",
"@toplevelentries": "26",
"viewentry": [
{
"@position": "1",
"@noteid": "8000013C",
"@children": "1277",
"@descendants": "1277",
"@siblings": "26",
"entrydata": [
{
"@columnnumber": "0",
"@name": "$3",
"@category": "true",
"text": {
"0": "A"
}
},
{
"@columnnumber": "1",
"@name": "InView",
"number": {
"0": "1277"
}
},
{
"@columnnumber": "2",
"@name": "FullName",
"text": {
"0": ""
}
},
{
"@columnnumber": "3",
"@name": "LastName",
"text": {
"0": ""
}
},
{
"@columnnumber": "4",
"@name": "FirstName",
"text": {
"0": ""
}
}
]
},
{
"@position": "1.1",
"@unid": "B933790B1DC265ED8025725800728CC5",
"@noteid": "1D612",
"@siblings": "1277",
"entrydata": [
{
"@columnnumber": "1",
"@name": "InView",
"number": {
"0": "1"
}
},
{
"@columnnumber": "2",
"@name": "FullName",
"text": {
"0": "Adam Aaron/ROCKALL"
}
},
{
"@columnnumber": "3",
"@name": "LastName",
"text": {
"0": "Aaron"
}
},
{
"@columnnumber": "4",
"@name": "FirstName",
"text": {
"0": "Adam"
}
}
]
},
{
"@position": "1.2",
"@unid": "9D93E80306A7AA88802572580072717A",
"@noteid": "191AA",
"@siblings": "1277",
"entrydata": [
{
"@columnnumber": "1",
"@name": "InView",
"number": {
"0": "1"
}
},
{
"@columnnumber": "2",
"@name": "FullName",
"text": {
"0": "Dave Aaron/ROCKALL"
}
},
{
"@columnnumber": "3",
"@name": "LastName",
"text": {
"0": "Aaron"
}
},
{
"@columnnumber": "4",
"@name": "FirstName",
"text": {
"0": "Dave"
}
}
]
},
{
"@position": "1.3",
"@unid": "FAFA753960DB587A80257258007287CF",
"@noteid": "2776A",
"@siblings": "1277",
"entrydata": [
{
"@columnnumber": "1",
"@name": "InView",
"number": {
"0": "1"
}
},
{
"@columnnumber": "2",
"@name": "FullName",
"text": {
"0": "Donnie Aaron/ROCKALL"
}
},
{
"@columnnumber": "3",
"@name": "LastName",
"text": {
"0": "Aaron"
}
},
{
"@columnnumber": "4",
"@name": "FirstName",
"text": {
"0": "Donnie"
}
}
]
},
{
"@position": "1.4",
"@unid": "3A408A7EFED21FBA8025725800727468",
"@noteid": "9ABA",
"@siblings": "1277",
"entrydata": [
{
"@columnnumber": "1",
"@name": "InView",
"number": {
"0": "1"
}
},
{
"@columnnumber": "2",
"@name": "FullName",
"text": {
"0": "Glen Aaron/ROCKALL"
}
},
{
"@columnnumber": "3",
"@name": "LastName",
"text": {
"0": "Aaron"
}
},
{
"@columnnumber": "4",
"@name": "FirstName",
"text": {
"0": "Glen"
}
}
]
},

Well, I dont know about you but I think the first example is much easier to read then the notes way of doing things. So now we need to find a way to get the view to render the JSON just as we want it to be rendered.

Because I wanted to know more about the Rest service and the use of beans in this extlib component I decided I wanted to populate the dijit.tree using this technique. If you want to know more on how I achieved this. Stay tuned for part 2.

Serving media files to iPad/iPhone on Domino part 2

Finally, after almost 2 months I finally have time to blog a bit again. This blog will be the second part in a 2 part series about how to serve files for iPad / iPhone devices on a domino server.

Back in part 1 I talked about an xpage application I was developing where the customer wanted to serve video streams to mobile devices. After a couple of days development I got a nice little application running where video’s could be uploaded and watched on all major browsers. The site used the video tag from HTML5 to serve the video files to the browser which accepted this tag. As said in Part 1 mobile safari uses the byte range header to retrieve the first few bytes of a stream to see which type of file is being streamed. By default, as far as I know, the domino server does not understand this header so we have to build this ourselves.

The first step in building this functionality is to create a simple custom control. The custom control contains the following lines of code:

<xp:view>
<xp:this.afterPageLoad>
<![CDATA[${javascript:var strUrl = context.getUrl().toString();
var i = strUrl.lastIndexOf("/");
var siteUrl = strUrl.substr(0,i);
		try{

viewScope.MediaFile = siteUrl+"/VideoTest.xsp?ID="+param.get("ID")+"&file=video.mpg";
viewScope.startImage = siteUrl+"/video_first_frame.png";
}catch(e){

}}]]></xp:this.afterPageLoad>
<xp:text id="html5Player" escape="false"  style="display: none;">
			<xp:this.value><![CDATA[#{javascript:var buff = new java.lang.StringBuffer();
buff.append("<video id=\"my_video_1\"");
buff.append("class=\"video-js vjs-default-skin\"");
buff.append("controls preload=\"auto\"");
buff.append("poster=\""+startImage+"\" data-setup=\"{}\">");
buff.append("<source src=\""+viewScope.MediaFile+"\">");
buff.append("</video>");
return buff.toString();

}]]></xp:this.value>
		</xp:text>

</xp:view>

As you can see nothing to fancy, an xp:text box which generates the correct html for the video tag. In the afterPageLoad I generate the correct url for for the video to be played. As you already notice I’m using an xPage as the url for the video tag src attribute. Lets see what happens on the this videoTest page?

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core" xmlns:xc="http://www.ibm.com/xsp/custom">
<xp:this.beforeRenderResponse><![CDATA[#{javascript:
		VideoFileRangeUtil.renderResponse(facesContext,param.get("ID"),param.get("file")); facesContext.responseComplete();}]]></xp:this.beforeRenderResponse>
</xp:view>

The VideoRangeUtil.renderResponse method is called with the current facescontext, the id of the document and the filename we want to retrieve. The renderReponse method looks like this. I have added comments on important places. This code was not completely written by me but was inspired by code from balusC.

Just be aware that this code is not perfect and can be optimized here and there.

package yourpackage.util;

//importes

public class VideoFileRangeUtil {

	public static void renderResponse(FacesContext ctx, String docUNID, String filename) throws IOException, ReadContextException{
		System.out.println("get File range data");
		Document fileDocument = null;
		EmbeddedObject attachment = null;
		try{
			System.out.println("Retrieve external context etc..");

			DominoExternalContext extcon = (DominoExternalContext) FacesContext.getCurrentInstance().getExternalContext();
			HttpServletRequest request = (HttpServletRequest) extcon.getRequest();
			HttpServletResponse response= (HttpServletResponse) extcon.getResponse();

			System.out.println("Retrieve document from content item: "+docUNID);
			ReadContext item = factory.getReadContext(docUNID);
			fileDocument = item.getDocument();

			System.out.println("Retrieve file attachment");
			attachment = getFileAttachment(ctx, filename,fileDocument);
			if(attachment == null){

				response.sendError(404);
				return;
			}

			System.out.println("Retrieve file information");
			long length = attachment.getFileSize();
			long lastModified = fileDocument.getLastModified().toJavaDate().getTime();
			String eTag = filename + "_" + length + "_" + lastModified;

	        String ifNoneMatch = request.getHeader("If-None-Match");
	        if (ifNoneMatch != null && VideoFileRangeUtilHelpers.matches(ifNoneMatch, eTag)) {
	            response.setHeader("ETag", eTag); // Required in 304.
	            response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
	            return;
	        }

	        System.out.println("Check last modified");
	        // If-Modified-Since header should be greater than LastModified. If so, then return 304.
	        // This header is ignored if any If-None-Match header is specified.
	        long ifModifiedSince = request.getDateHeader("If-Modified-Since");
	        if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {
	            response.setHeader("ETag", eTag); // Required in 304.
	            response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
	            return;
	        }

	        System.out.println("Check if-match headers");
	        String ifMatch = request.getHeader("If-Match");
	        if (ifMatch != null && !VideoFileRangeUtilHelpers.matches(ifMatch, eTag)) {
	            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
	            return;
	        }

	        System.out.println("Check If-Unmodified-Since headers");
	        long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");
	        if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {
	            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
	            return;
	        }
	        System.out.println("Create byte ranges");
	        ByteRange full = new ByteRange(0, length - 1, length);
	        List ranges = new ArrayList<ByteRange>();

	        System.out.println("Get range headers");
	        // Validate and process Range and If-Range headers.
	        String range = request.getHeader("Range");
	        if (range != null) {

	            // Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
	            if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
	                response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
	                response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
	                return;
	            }

	            // If-Range header should either match ETag or be greater then LastModified. If not,
	            // then return full file.
	            String ifRange = request.getHeader("If-Range");
	            if (ifRange != null && !ifRange.equals(eTag)) {
	                try {
	                    long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid.
	                    if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) {
	                        ranges.add(full);
	                    }
	                } catch (IllegalArgumentException ignore) {
	                    ranges.add(full);
	                }
	            }

	            System.out.println("Proces part of range");
	            // If any valid If-Range header, then process each part of byte range.
	            if (ranges.isEmpty()) {
	                for (String part : range.substring(6).split(",")) {
	                    // Assuming a file with length of 100, the following examples returns bytes at:
	                    // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
	                    long start = VideoFileRangeUtilHelpers.sublong(part, 0, part.indexOf("-"));
	                    long end = VideoFileRangeUtilHelpers.sublong(part, part.indexOf("-") + 1, part.length());

	                    if (start == -1) {
	                        start = length - end;
	                        end = length - 1;
	                    } else if (end == -1 || end > length - 1) {
	                        end = length - 1;
	                    }

	                    // Check if Range is syntactically valid. If not, then return 416.
	                    if (start > end) {
	                        response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
	                        response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
	                        return;
	                    }

	                    // Add range.
	                    ranges.add(new ByteRange(start, end, length));
	                }
	            }

	        }

	        String contentType = "video/x-m4v";
	        boolean acceptsGzip = false;
	        String disposition = "inline";

	        // If content type is unknown, then set the default value.
	        // For all content types, see: http://www.w3schools.com/media/media_mimeref.asp
	        // To add new content types, add new mime-mapping entry in web.xml.
	        if (contentType == null) {
	            contentType = "application/octet-stream";
	        }

	        // If content type is text, then determine whether GZIP content encoding is supported by
	        // the browser and expand content type with the one and right character encoding.
	        if (contentType.startsWith("text")) {
	            String acceptEncoding = request.getHeader("Accept-Encoding");
	            acceptsGzip = acceptEncoding != null && VideoFileRangeUtilHelpers.accepts(acceptEncoding, "gzip");
	            contentType += ";charset=UTF-8";
	        } 

	        // Else, expect for images, determine content disposition. If content type is supported by
	        // the browser, then set to inline, else attachment which will pop a 'save as' dialogue.
	        else if (!contentType.startsWith("image")) {
	            String accept = request.getHeader("Accept");
	            disposition = accept != null && VideoFileRangeUtilHelpers.accepts(accept, contentType) ? "inline" : "attachment";
	        }

	        // Initialize response.
	        response.reset();
	        response.setBufferSize(VideoFileRangeUtilHelpers.DEFAULT_BUFFER_SIZE);
	        response.setHeader("Content-Disposition", disposition + ";filename=\"" + filename + "\"");
	        response.setHeader("Accept-Ranges", "bytes");
	        response.setHeader("ETag", eTag);
	        response.setDateHeader("Last-Modified", lastModified);
	        response.setDateHeader("Expires", System.currentTimeMillis() + VideoFileRangeUtilHelpers.DEFAULT_EXPIRE_TIME);

	        // Send requested file (part(s)) to client ------------------------------------------------

	        // Prepare streams.
	        InputStream input = attachment.getInputStream();
	        OutputStream output = null;

	        try {
	            // Open streams.

	            output = response.getOutputStream();
	            InputStream str;

	            if (ranges.isEmpty() || ranges.get(0) == full) {

	                // Return full file.
	                ByteRange r = full;
	                response.setContentType(contentType);
	                response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);

	                if (acceptsGzip) {
	                        // The browser accepts GZIP, so GZIP the content.
	                        response.setHeader("Content-Encoding", "gzip");
	                        output = new GZIPOutputStream(output, VideoFileRangeUtilHelpers.DEFAULT_BUFFER_SIZE);
	                    } else {
	                        // Content length is not directly predictable in case of GZIP.
	                        // So only add it if there is no means of GZIP, else browser will hang.
	                        response.setHeader("Content-Length", String.valueOf(r.length));
	                    }

	                    // Copy full range.
	                    VideoFileRangeUtilHelpers.copy(input, output, r.start, r.length);

	            } else if (ranges.size() == 1) {

	                // Return single part of file.
	                ByteRange r = (ByteRange)ranges.get(0);
	                response.setContentType(contentType);
	                response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
	                response.setHeader("Content-Length", String.valueOf(r.length));
	                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

	                    // Copy single part range.
	                VideoFileRangeUtilHelpers.copy(input, output, r.start, r.length);

	            } else {

	                // Return multiple parts of file.
	                response.setContentType("multipart/byteranges; boundary=" + VideoFileRangeUtilHelpers.MULTIPART_BOUNDARY);
	                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

	                    // Cast back to ServletOutputStream to get the easy println methods.
	                    //ServletOutputStream sos = (ServletOutputStream) output;

	                    // Copy multi part range.

//	                    for (ByteRange r : ranges) {
//	                        // Add multipart boundary and header fields for every range.
//	                        sos.println();
//	                        sos.println("--" + VideoFileRangeUtilHelpers.MULTIPART_BOUNDARY);
//	                        sos.println("Content-Type: " + contentType);
//	                        sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);
//
//	                        // Copy single part range of multi part range.
//	                        VideoFileRangeUtilHelpers.copy(input, output, r.start, r.length);
//	                    }
//
//	                    // End with multipart boundary.
//	                    sos.println();
//	                    sos.println("--" + VideoFileRangeUtilHelpers.MULTIPART_BOUNDARY + "--");

	            }
	        } finally {
	            // Gently close streams.
	        	VideoFileRangeUtilHelpers.close(output);
	        	VideoFileRangeUtilHelpers.close(input);
	        }

		}catch(NotesException e){
			e.printStackTrace();
		}finally{
			try{
				attachment.recycle();
				fileDocument.recycle();

			}catch(NotesException e){
				e.printStackTrace();
			}
		}

	}

/* Given the filename and the document datasource retrieve the correct embembeddedObject. */

	private static EmbeddedObject getFileAttachment(FacesContext ctx,
			String filename, Document fileDocument) throws NotesException {
		System.out.println("Retrieve file "+filename);
		if(fileDocument == null){
			return null;
		}
		System.out.println("Get attachment by filename");
		EmbeddedObject attachment = fileDocument.getAttachment(filename);

		System.out.println(attachment.getName());
		if(attachment == null){
			System.out.println("Return nothing");
			return null;
		}
		return attachment;
	}

}

 

Serving media files to iPad/iPhone on Domino part 1

 

“We want a video library application” they said

“No problem, can be done” I said

“Bad iPad” I screamed!

the lines above are a  little summary of what happened the last few days. At the office  I’m building  a web application which is used to serve Video files. Everything worked fine except for iPhone and iPad devices. I needed to get it working. At first I thought it was a simple encoding issue. So I grabbed my MediaInfo installation, checked the codec information on the mp4 file(s) and spotted that the wrong baseline was used. It was 4 and according to the official apple sources the baseline level needs to be 3.0 (source). So I re-encoded the file and uploaded the new version. I fired up the iPad and noted that the file still didn’t work. Ah! Maybe it was browser caching . So I renamed the file, uploaded it and again.. nothing. On all devices/browser it worked (ie 9, firefox, chrome, safari on windows, my android phone..) except for the freaking iPad!

So being out of options the only thing I could do was head over to stackoverflow and ask my question there. And so I did. Here I present to you a possible solution:

The server needs to support byte-range requests for the mobile Safari ( which is installed on iOs devices ) to be able to play video files using HTML 5.  Byte-range requests? What are byte-range requests I here you think. Actually its fairly easy to explain.

A byte range request is actually nothing more then sending only a little part of the complete file to the client (more). To be able to support this feature a server must react on the range request header and send the Accept-Ranges header accordingly. Because I didn’t know of this requirement I was really puzzled to why the file played correctly on my local Apache install and not on my domino server. To make it even more exciting I tested the following situations

Description Result
File resource Correct
File resource in domino/html Correct
File attachment on document Fail!

As you can see for some reason when serving attachments to the browser the correct headers are not send to the browser. I tried to add them by website rules but of course this failed. Since the system needs to support it as well. So I checked back my question on Stackoverflow to see if there where new additions. And Sven added a little code resource to his answer which actually does exactly that what I wanted.

The code example uses a servlet to serve the file using the range headers if it was requested or the full file if not. I’ve adapted the code and changed it a bit so it is usable for the domino server ( see part 2 for the full code example and explanation ). When you call the code in a beforerenderresponse event with the following line

ByteRangeUtil.renderResponse(facesContext,docunid,filename);

it will serve the correct file with byte-range support! How cool is that!

In the next Part I will explain to you which code I used and will show you what I changed to get it working for Domino/XPages

Source control to the rescue

For those who own a Qnap NAS system and want to use source control. But dont want to use public repositories like Git: This wiki article explains how to install and run svn on your local qnap. Just be aware that the passwords are by default saved as plain text so be sure your qnap is not directly accesible via the web or so.

I took me less then 5 minutes to create a svn server, a repository and to add my projects from my eclipse client ( indigo ) to the svn server. As a test I removed the project from my eclipse after I posted it to SVN. I created a new project from svn and everything worked fine.

Because my own Qnap is syncing with the Qnap of my family 100km away I can assure now my projects are save.

Next step: Add svn to my domino designer 9 install!

Don’t forget the milliseconds. A post about dates and Datetime comparison

During my work I find working with date and time objects one of the most challenging things. I just spent a few hours finding out that when you retrieve date from a view column the millisecond part of the date is actually not set at all.

In my current project I’m using a date control to set the date of a certain object. After processing I save this data into notesdocument. I noticed when I switched dates ( using a GregorianCalendar ) and retrieved data from the view the documents didn’t turn up on the screen. After some investigation I found out that when using the date-time control for some reason the date is saved but the milliseconds part of the date is not saved at all. This can lead to some frustation.

Let’s show you a little example

Lets say we have the following code

 

GregorianCalendar cal = GregorianCalendar.getInstance()
cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); cal.set(Calendar.HOUR_OF_DAY,0); 
cal.set(Calendar.MINUTE,0); 
cal.set(Calendar.SECOND,0); 

Date startDate = cal.getTime(); 
View x = Database.getView("testview");
ViewEntryCollection coll = x.getAllEntries();
ViewEntry entr = coll.getFirstEntry();
while (entr != null) { 
    System.out.println("Entry processing"); 
   Vector<?> values = entr.getColumnValues(); 
   Object o = values.get(2); 
   DateTime dt = null;
   if(o instanceof DateTime){
      dt = (DateTime)o;
    } 
   if (dt != null) { 
     Date lineDate = dt.toJavaDate(); 
     System.out.println("---"); 
     System.out.println("line date "+lineDate); 
     System.out.println(lineDate.getTime()); 
     System.out.println(startDate.getTime());  // should be the same as linedate ?
   }
  entr = coll.getNextEntry(entry);
}

System.out.println("Done!")

 

At first glance you might think that the lineDate.getTime and the startDate.getTime() would return the same integer if they point to the same date f.i. 31/12/2012 but in fact they wont..

In my particular case. The lineDate.getTime() returned 1356994800000 and startDate.getTime()  returned 1356994800640. As you can see a difference of 640 milliseconds. This is caused by my first call to the GregorianCalendar.getInstance(). It creates a new calendar instance with the date set to the current date /time. So at a specific point in time there are 640 milliseconds at the end.

When you now compare these two methods and use the system.out.println(linedate) directive you will see something like Mon Dec 31 00:00:00 CET 2012 which is the string representation of that date. Which is the same as system.out.println(startDate). But it wont show you the milliseconds! It took me a little while to realize the millisecond part could be different.

If you know this fact you can work around it by adding 1 line to the above example:

getCalendar().set(Calendar.MILLISECOND, 0);

 

I hope this blogpost helps someone in the future when working with dates in xPages/Java.

Working with Notes designer 9.0

IBM released their next version of Notes in public beta status on the 14th. Since then I’m busy playing around with the new client / designer on my home development box. I have to say they did a great job once again. The client starts very fast compared to the 8.5.x releases and the UI is a big change.

But what’s in for me as notes developer? And especially xPages? At first I was a bit disappointed. I opened up the designer and didn’t really saw something new. So I decided to look around on the net to see if fellow notes dev’s already played around with the designer and of course they did! I watched the excellent video on notesin9.com. I would say take a look yourself. But If you don’t want to watch a movie keep on reading. The new features in this notes designer are as follows:

  1. New Jar component in the notes designer.
    • Now you can add easily your jar files you need to the nsf by clicking on the import jar button in the following screen. Unfortunately this is only useful for xPages and can’t be used by Java agents. I hope sometime IBM decides to change this.jar
  2. Content assist in xPage source Editor!!
    • This is actually one of the best, or even the best, new feature in this version of designer. Now it is possible, when you are editing xPages using the source editor to issue type ahead. So for instance for SSJS. I haven’t been able to get it working for own Java classes yet. But I do hope there is somebody out there who knows if this is going to be possible or is possible already.
  3. Hyperlink navigation in xPage source editor
    • In default eclipse when developing Java you can ‘jump’ to another class / or its interface, by holding the CTRL key and click on it. This is very cool feature because you now can go to a separate part of your code without having to search for it in the package explorer. Finally this feature is available in the xPage source editor. How cool is that! No searching in your controls/xpage section anymore.( this is a pain when using lots of controls ).

As you can see alot of new features which will make the life of us developers a lot easier. I have to say after 3 days of playing around with the new designer I really don’t want to go back to 8.5.3 when I’m developing xPages. Because on top of all these great new features i’m noticing a big improvement in times I have to wait for my designer to become responsive again when I pressed ‘build project’.

But it’s a beta so there are some bugs! Atleast I found some.

1. I disabled ‘build automatically since this increases my productivity. When I want to see the result of my change(s) I build the project and go to my webbrowser. What I found is that somehow the build project option is disabled in the project menu. To reconstruct the issue:  Open up a database in designer.

Open a xpage/custom control in source mode.

Close the working set pane and click again the source editor.

Sometimes the build project option will be grayed out and you have to select the project again ( in the working set pane ) to be able to build.

2. Content assist does not work for Java and SSJS resources

As stated above the content assist feature is very very cool and makes it easier. But the content assist is not aware of custom classes (java) I created. It is also not aware of functions defined in included resources. I can imagine this would be a performance drain because you have to parse the SSJS libraries but it would be nice if it could be included.

Tagging to the rescue

A few weeks ago I started to think about digitizing my administration. So any invoice, tax mail or any other official document that would otherwise disappear in one of my big filing cabinets would now be scanned and saved onto my computer.

So I installed truecrypt to be sure the files are being saved encrypted and started to scan the documents. After a while I realized.. wait.. how do i search in all this data? My first thought was ‘tagging’.

So today after a little search on google I decided to create my own litle tagging application. I present to you Tagger. Just be warned. I wrote it just in a few hours and therefore I would call it a beta release. Main features for this application are simple:

  • List files in a certain directory where the program is running from
  • Add tags to the files
  • Search files by tags
  • Save the tag data in a filesystem independent manner.

Features not yet implemented

  • Remove tags from file
  • Show a list of most used tags

Go ahead and download Tagger and tell me what you think !

oh by the way.. the UI is terrible.. I know

Java: Integrating onTime group calendar in your own application

One thing I love about my work is that now and then I get the chance to play with the newest things on the market. Recently I took a look at the Ontime suite. Ontime suite is a application which enables a company, amongst other things, to display a overview of their employee’s appointments without having to open every single agenda. Since a few months the guys at Ontime released a new version which is features a API to communicate with the Ontime application directly using http.

The cool thing about this API is that you can use it yourself to connect to the Ontime application functionality. For instance if you want to add the possibility inside your Eclipse client, Joomla application or WordPress blog to schedule a meeting or display meeting information you can use this API to do so. Currently I’m looking into the possibilities and it looks very promising.

Since I mainly develop in Java during such a R&D project, I created an JAVA wrapper around the API database. I planning to use this wrapper inside my custom Java application to display calendar appointments and create appointments for a certain user.

If this works I can use this wrapper in all Java based applications to communicate with the Ontime API without having to install a AJAX Proxy ( since ajax can’t communicate with different domains and a server based solution can. ) etc.

The cool thing is of course that this opens up possibilities for everyone who has the Ontime suite to integrate its functionality within their own applications!

Responsive webdesign and xPages

As a xpage developer I also need to do some styling from time to time. This isnt a big issue ofcourse unless you need to create an app which is also suitable for mobile devices. Now with the mobile controls at hand it isn’t a big problem developing a mobile website but what if you want to create a website which should look good on mobile and desktop devices and you don’t want to build a mobile and a desktop version of your app?

Here is where responsive webdesign comes in. I’m just learning this myself ( since I’m not a great UI guy.. ) and I already like it. In it’s simplest form all it comes down to is just to add some extra lines to your css files..

Lets say you use the oneui template and you want to hide certain parts when you are viewing the page on an Iphone. You only need to add the following line to the css:

@media screen and (max-width:320px) {
    .lotusRightBar { display: none; }
  }

 

As you can see the right sidebar is not displayed when the size of the screen is les or equal to 320px width. In this very simple way you can add your responsive design to the oneui template without having to create a complete new website for mobile. Unless functionality commands it ofcourse 😉