March 11, 2009

JavaScript Depot Timeline Generator

Healthcare
MERGE User Conference
Traceability

I saw an interesting project the other day for a JavaScript timeline that plots either a JSON or XML event model using Ajax requests to retrieve the data..  This seemed like a great mashup opportunity to combine the timeline framework with Perforce changelist data for a given depot path.  The server side code is written completely in Java using a single servlet and Jetty as the HTTP server. The servlet calls p4 changes and p4 describe for the requested depot path to build a JSON model that the Timeline API uses to plot the events.  The front-end of this web application is a text box that accepts a depot path to a folder or file and when the user clicks the generate timeline button an Ajax request is made to the Java servlet that retrieves the changelist event model that is then plotted on the timeline.

The timeline interaction is very similar to Google Maps, use the mouse to drag to different areas and double-clicking centers on the selected area. The top timeline is showing day-by-day changes while the bottom timeline can be used to move month-by-month. When a changelist is selected a bubble will pop up showing the changelist description and date and also a link that will take you to the changelist's corresponding P4Web page.

Shown below is a screenshot of a changelist timeline generated against the public Perforce depot for the //public/jam folder.

depot timeline public server

To install and run

  1. Download DepotTimelineServlet.java
  2. Download DepotTimelineServer.java
  3. Download depot_timeline.html
  4. Download depot_timeline.js
  5. Download Jetty 6.1 or later (Tested with Jetty 6.1.12)
  6. Configure the constants at the top of DepotTimelineServlet
    • P4_PATH = <Location of P4 executable>
    • P4_SERVER =<Perforce server address>
    • P4_USER =<Perforce user>
    • WEB_LINK_START = <Edit the HTTP address for your depot's P4Web location>
  7. Set the 'base' System property to the absolute address on disk of the folder containing the depot_timeline.html file. Or just change the String base variable inside DepotTimelineServer.java.
  8. Compile the two Java files and run DepotTimelineServer
  9. Navigate a browser to localhost:8081/depot_timeline.html
  10. Enter a depot path in the text box (remember to end folder depot paths with /...)
  11. Select the Generate Timeline button and the timeline will be generated

Note: The entire depot_timeline project is in the Perforce public depot so another way to get and run this project would be to download Eclipse and sync and import the depot_timeline project using P4WSAD. Read the timeline how-to if you want to edit the look and feel of the timelines created in depot_timeline.js. Code snippets are shown below for the main servlet code that generates the JSON used by the timeline framework, the HTTP server creation code, main HTML page, and JavaScript timeline  file.

JavaScript code to setup the look and feel of the timeline and request the data:

var t1;var resizeTimerID =null;var eventSource =new Timeline.DefaultEventSource(0);var bandInfos =null;functiononResize(){if(resizeTimerID ==null){
 	    resizeTimerID = window.setTimeout(function(){
            resizeTimerID =null;
            tl.layout();},500);}}functiononLoad(){var theme = Timeline.ClassicTheme.create();
    theme.event.bubble.width =200;
    theme.event.label.width =200;
    theme.event.bubble.height =120;
    bandInfos =[
        Timeline.createBandInfo({
            eventSource: eventSource,
            date:newDate(),
            width:"70%",
            intervalUnit: Timeline.DateTime.DAY,
            intervalPixels:100,
            theme: theme
		}),
		Timeline.createBandInfo({
            eventSource: eventSource,
            date:newDate(),
            width:"30%",
            intervalUnit: Timeline.DateTime.MONTH,
            intervalPixels:200,
            showEventText:false,
            theme: theme
        })];
    bandInfos[1].syncWith =0;
    bandInfos[1].highlight =true;

    tl = Timeline.create(document.getElementById("timeline"),
            bandInfos, Timeline.HORIZONTAL);}functiongenerateTimeline(){var text = document.getElementById("path").value;
    tl.loadJSON("timeline?path="+text,function(json, url){
        eventSource.clear();
        eventSource.loadJSON(json, url);});}

Main servlet code that generates a JSON HTTP response for the changelist data for the requested depot path:

privatevoidhandleFiles(HttpServletRequest request,
  HttpServletResponse response)throws IOException, ServletException {
	Map<Object, Object> responseMap =new HashMap<Object, Object>();

	String path = request.getParameter("path");if(path !=null){
	    List<String> commands =new ArrayList<String>(BASE_COMMAND);
	    commands.add("changes");
	    commands.add(path);
	    ProcessBuilder builder =newProcessBuilder(commands);
	    Process process = builder.start();

	    List<Object> events =new ArrayList<Object>();
	    responseMap.put("events", events);

	    List<String> changes =new ArrayList<String>();try{
		BufferedReader reader =newBufferedReader(newInputStreamReader(process.getInputStream()));
		String line = reader.readLine();while(line !=null){
		    line = line.trim();if(line.startsWith("Change ")){
			line = line.substring(7);
			line = line.substring(0, line.indexOf(' '));
			changes.add(line.trim());}
		    line = reader.readLine();}
		reader.close();
		process.waitFor();

		List<String> describeCommands =new ArrayList<String>(
			BASE_COMMAND);
		describeCommands.add("describe");
		describeCommands.add("-s");
		describeCommands.addAll(changes);
		builder =newProcessBuilder(describeCommands);
		process = builder.start();
		reader =newBufferedReader(newInputStreamReader(process
			.getInputStream()));
		line = reader.readLine();
		StringBuffer description =null;
		Map<Object, Object> event =null;while(line !=null){if(line.startsWith("Change ")){
			description =newStringBuffer();
			line = line.substring(7);
			String user = line.substring(line.indexOf("by ")+3);
			user = user.substring(0, user.indexOf('@'));
			String change = line.substring(0,
                                          line.indexOf(' '));
			String date = line
				.substring(line.lastIndexOf("on")+2);
			date = date.trim();

			event =new HashMap<Object, Object>();
			event.put("start", date);
			event.put("title", change +" by "+ user);
			event.put("description", change);
			event.put("link", WEB_LINK_START + change
				+ WEB_LINK_END);
			events.add(event);}elseif((line.startsWith("Jobs fixed ...")|| line
			    .startsWith("Affected files ..."))&&(event !=null&& description !=null)){
			event.put("description", description);
			description =null;}elseif(description !=null){
			description.append(line +" ");}
		    line = reader.readLine();}
		reader.close();
		process.waitFor();}catch(InterruptedException e){}}
	String json = JSON.toString(responseMap);
	response.getWriter().print(json);
	response.setStatus(200);
	response.setContentLength(json.length());
	response.setContentType("application/json");}

HTTP server code that adds the servlet and starts the server:

publicstaticvoidmain(String args[]){
	Server server =newServer();
	SocketConnector connector =newSocketConnector();
	connector.setPort(8081);
	server.setConnectors(new Connector[]{ connector });try{
	    String base = System.getProperty("base");
	    DefaultServlet servlet =newDefaultServlet();
	    ServletHolder sHolder =newServletHolder(servlet);
	    sHolder.setInitParameter("dirAllowed","true");
	    sHolder.setInitParameter("resourceBase", base);
	    Context context =newContext(server,"/", Context.SESSIONS);
	    context.addServlet(sHolder,"/*");
	    context.addServlet(DepotTimelineServlet.class,"/timeline");
	    server.start();
	    server.join();}catch(Exception e){
	    e.printStackTrace();}}

HTML depot timeline page:

<html><head><title>Depot Timeline</title><style>.timeline-event-bubble-title,.timeline-event-bubble-body,.timeline-event-bubble-time {font-size:10pt;
        }.depot-timeline {font-family:TrebuchetMS, Helvetica, Arial, sansserif;
            font-size:8pt;
            border:1pxsolid#aaa;
            height:500px;
        }#search{margin-top:10px;
            margin-bottom:15px;
        }</style><scriptsrc="http://simile.mit.edu/timeline/api/timeline-api.js"type="text/javascript"></script><scriptsrc="depot_timeline.js"type="text/javascript"></script></head><bodyonresize="onResize()"onload="onLoad()"><divid="search">
Depot path: <inputid="path"type="text"style="width:300px;"/><inputtype="button"value="Generate Timeline"onclick="generateTimeline();"/></div><divclass="depot-timeline"id="timeline"></div></body></html>