Customize Tomahawk scheduler to support grouped entries and lazy loading

I am using myfaces (1.1.5) in my web application, and I was in the need of having some sort of a calendar/scheduler where the user would be able to add multiple records from one category/group/datatype. Imagine that you have multiple collections of some types of data, and each item within each type have date properties that could be presented in a calendar. I checked the scheduler of the Tomahawk library (1.1.6), but this was not supported by default. You could only add “standalone” entries, with no possibility of grouping them together.

Still, I decided to give it a try, and wanted to extend the scheduler to support this functionality. Another thought I had was that I wanted to only retreive the data I needed from the server, in other words: If a datatype have several thousand items, but in the month I’m currently viewing there are really just 18 representations, I would like to only fetch those 18 instances, not the whole collection every time.

In the following example I’ve only done the customization for the “month” mode, as this was the one of importance to me. But it should not be difficult to do this also for the other modes if that is needed.

To start, I decided to create a new class called SchedulerEntryGroup. Basically you can think of this as a group that holds from n to many items that should be entries in our calendar.


public class SchedulerEntryGroup
{
    private String name;
    private String dataTypeName;
    private List<Item> items;
    private String color;
    private boolean visible;
    private String dateProperty;
    private String displayProperty;

   public SchedulerEntryGroup()
    {
        items = new ArrayList<Items>();
    }

    publc SchedulerEntryGroup(String name, String color, boolean visible, String dataTypeName, String dateProperty,String displayProperty)
    {
        setName(name);
        setColor(color);
        setVisible(visible);
        setDateProperty(dateProperty);
        setDisplayProperty(displayProperty);
        setDataTypeName(dataTypeName);

    }

   .........
   SET/GET METHODS
   ......... 

}

As you can see the group holds properties like name (label), dataTypeName (which object-type it contains), the list of items and if it should be visible or not (to enable the possibility of activating/deactivating the displayed entries of a group with a checkbox e.g). It also contains a color property, as this should be common for all the entries of the same group. The dateProperty says which property that contains the dates, and the displayProperty which property should be displayed for the entry in the calendar.In my world the Item object holds a map with objects, and that’s why I pass properties as Strings to get them from the object. It’s basically the key in the properties-map. For reference a “short-version” of my Item-class looks like this:


public class Item
{
    private Map<String,Object> properties;

    public Item()
    {
        properties = new HashMap<String,Object>();
    }

    public Object getProperty(String key)
    {
        return properties.get(key);
    }

}

Ok, I have my entry-group now, but I also made a small change to the Entry class itself, as I wanted to have a color property. I extended the DefaultScheduleEntry and created a class SchedulerEntryExtended, the only difference adding a property color:


public class SchedulerEntryExtended extends DefaultScheduleEntry
{
    private String color;

    public SchedulerEntryExtended()
    {
        super();
    }

    public SchedulerEntryExtended(String description, boolean allDay, Date startTime, Date endTime, String title, String subTitle, String color)
    {
        super();
        super.setDescription(description);
        super.setAllDay(allDay);
        super.setStartTime(startTime);
        super.setEndTime(endTime);
        super.setTitle(title);
        super.setSubtitle(subTitle);
        setColor(color);
    }

    public String getColor()
    {
        return color;
    }
    public void setColor(String color)
    {
        this.color = color;
    }
}

Then I had to change the rendering of entries in month-mode, as I want the group’s color to be set as background-color for each entry. To do this I extended and overrode the DefaultScheduleEntryRenderer and created the class ScheduleEntryRenderer. There are two methods I override, the getColor and the renderCompactContent. The getColor was originally implemented like this:


public String getColor(FacesContext context, HtmlSchedule schedule,
                           ScheduleEntry entry, boolean selected)
    {
        return null;
    }

And the renderCompactContent was implemented like this:


protected void renderCompactContent(FacesContext context, ResponseWriter writer, HtmlSchedule schedule, ScheduleDay day, ScheduleEntry entry, boolean selected) throws IOException
    {
        StringBuffer text = new StringBuffer();
        Date startTime = entry.getStartTime();

        if (day.getDayStart().after(entry.getStartTime()))
        {
            startTime = day.getDayStart();
        }

        Date endTime = entry.getEndTime();

        if (day.getDayEnd().before(entry.getEndTime()))
        {
            endTime = day.getDayEnd();
        }

        if (!entry.isAllDay())
        {
            DateFormat format = DateFormat.getTimeInstance(DateFormat.SHORT);
            text.append(format.format(startTime));
            if (!startTime.equals(endTime)) {
                text.append("-");
                text.append(format.format(endTime));
            }
            text.append(": ");
        }
        text.append(entry.getTitle());

        writer.writeText(text.toString(), null);
    }

My ScheduleEntryRenderer class came out like this:

public class SchedulerEntryRenderer extends DefaultScheduleEntryRenderer
{
     public String getColor(FacesContext context, HtmlSchedule schedule,
             ScheduleEntry entry, boolean selected)
     {

	        SchedulerEntryExtended ent = (SchedulerEntryExtended) entry;
         return ent.getColor();
     }

	    protected void renderCompactContent(FacesContext context, ResponseWriter writer, HtmlSchedule schedule, ScheduleDay day, ScheduleEntry entry, boolean selected) throws IOException
     {

	        StringBuffer text = new StringBuffer();
         Date startTime = entry.getStartTime();

	        if (day.getDayStart().after(entry.getStartTime()))
         {
             startTime = day.getDayStart();
         }

	        Date endTime = entry.getEndTime();

	        if (day.getDayEnd().before(entry.getEndTime()))
         {
             endTime = day.getDayEnd();
         }

	        if (!entry.isAllDay())
         {
         	DateFormat format = DateFormat.getTimeInstance(DateFormat.SHORT);
         	text.append(format.format(startTime));
         	if (!startTime.equals(endTime)) {
         		text.append("-");
         		text.append(format.format(endTime));
         	}
         	text.append(": ");
         }
         text.append(entry.getTitle());

	        StringBuffer entryStyle = new StringBuffer();
 		  entryStyle.append("height: 100%; width: 100%;");
 		  String entryColor = getColor(context, schedule, entry, selected);
 		  if (entryColor != null)
 		  {
 		      entryStyle.append("border-color: ");
 		      entryStyle.append(entryColor);
 		      entryStyle.append(";");
 		      entryStyle.append("background-color: ");
 		      entryStyle.append(entryColor);
 		      entryStyle.append(";");
 		  }
 		  writer.startElement(HTML.DIV_ELEM, null);
 		  writer.writeAttribute(HTML.STYLE_ATTR,entryStyle.toString(), null);
 		  writer.writeText(text.toString(), null);
 		  writer.endElement(HTML.DIV_ELEM);
 	}
}

Notice that in getColor() I cast and use my SchedulerEntryExtended to retrieve the color. What I do in the renderCompactContent method is that I create an entryStyle string, and you can see that I apply the entryColor both to the border and the background. Then I wrap the text inside a div, and apply the entryStyle on this div. Thanks for examples at IRIAN and the tomahawk-examples.zip to point me in the direction of how to do this.

Then the main thing that remains is to have a handler that holds the model for the calendar, as well as having business-logic for using our entry-groups.This is my ScheduleHandler, which have methods for adding, deleting, displaying and hiding groups, as well as the method that creates the model of the calendar, createModel(). The “container” for all the groups that is to be rendered is a linked hashmap named schedulerMap:


public class SchedulerHandler implements Serializable
{
    private static final long serialVersionUID = -8815771399735333108L;

    private ScheduleModel model;
    private Map<String, SchedulerEntryGroup> schedulerMap;
    private SchedulerEntryGroup entryGroupToDelete;
    private SchedulerEntryGroup entryGroupToHide;
    private SchedulerEntryGroup entryGroupToDisplay;
    private Date selectedDate;

    public SchedulerHandler()
    {
        model = new SimpleScheduleModel();
        model.setMode(3);
        model.setSelectedDate(new Date());
        schedulerMap = new LinkedHashMap<String, SchedulerEntryGroup>();
        selectedDate = new Date();
        createModel();
    }

    public Collection<SchedulerEntryGroup> getEntryGroups()
    {
        return schedulerMap.values();
    }

    public void addGroup(SchedulerEntryGroup entryGroup)
    {
        schedulerMap.put(entryGroup.getName(), entryGroup);
        createModel();
    }

    public void hideEntryGroup()
    {
           SchedulerEntryGroup group = schedulerMap.get(getEntryGroupToHide().getName());
        group.setVisible(false);
        schedulerMap.put(group.getName(), group);
        createModel();
    }

    public void displayEntryGroup()
    {
        SchedulerEntryGroup group = schedulerMap.get(getEntryGroupToDisplay().getName());
        group.setVisible(true);
        schedulerMap.put(group.getName(), group);
        createModel();
    }

    public void deleteEntryGroup()
    {
        if(schedulerMap.containsValue(getEntryGroupToDelete()))
        {
            schedulerMap.remove(entryGroupToDelete.getName());
            createModel();
        }
    }

    public ScheduleModel getModel()
    {
        return model;
    }

    public void setModel(ScheduleModel model)
    {
        this.model = model;
    }

   private void createModel()
    {

        setModel(new SimpleScheduleModel());

        int i = 0;
        String id = "";
        for(Entry<String, SchedulerEntryGroup> e : schedulerMap.entrySet())
        {
            id = "Entry_"+e.getValue().getName()+i;
            SchedulerEntryGroup entryGroup = e.getValue();
            if(entryGroup.isVisible())
            {

                List<Item> items = entryGroup.getItems();

                for(Item i : items)
                {
                    SchedulerEntryExtended entry = new SchedulerEntryExtended();
                    entry.setColor(entryGroup.getColor());

                     Object o = i.getProperty(entryGroup.getDateProperty());

                     String s = (String) o;
                     Date d = null;
                     if(StringUtils.isNotBlank(s))
                     {
                        try
                        {
                            d = DateUtil.parseDate((String)o);
                        } catch (ParseException pe)
                        {
                            // TODO Auto-generated catch block
                            pe.printStackTrace();
                        }
                        model.setSelectedDate(d);
                        entry.setId(id);
                        entry.setTitle(i.getProperty(entryGroup.getDisplayProperty()));

                        entry.setStartTime(d);
                        entry.setEndTime(d);
                        entry.setAllDay(true);
                        model.addEntry(entry);
                     }
                     i++;
                }
           }
        }

        if(getSelectedDate() != null)
            model.setSelectedDate(getSelectedDate());
        else
            model.setSelectedDate(new Date());
        model.setMode(3);
        model.refresh();
    }

      public SchedulerEntryGroup getEntryGroupToDelete()
    {
        return entryGroupToDelete;
    }
    public void setEntryGroupToDelete(SchedulerEntryGroup entryGroupToDelete)
    {
        this.entryGroupToDelete = entryGroupToDelete;
    }
    public Date getSelectedDate()
    {
        return selectedDate;
    }
    public void setSelectedDate(Date selectedDate)
    {
        if(selectedDate != null)
        {
            this.selectedDate = selectedDate;
            getModel().setSelectedDate(selectedDate);
            createModel();
        }
    }
    public SchedulerEntryGroup getEntryGroupToHide()
    {
        return entryGroupToHide;
    }
    public void setEntryGroupToHide(SchedulerEntryGroup entryGroupToHide)
    {
        this.entryGroupToHide = entryGroupToHide;
    }
    public SchedulerEntryGroup getEntryGroupToDisplay()
    {
        return entryGroupToDisplay;
    }
    public void setEntryGroupToDisplay(SchedulerEntryGroup entryGroupToDisplay)
    {
        this.entryGroupToDisplay = entryGroupToDisplay;
    }

}

To add a group just create a new instance of it, fill up and set the Item-collection, and pass the group as an argument to the addGroup()-method like this:


public void addEntryGroup()
{
        boolean visible = true;
        SchedulerEntryGroup entryGroup = new SchedulerEntryGroup("myGroup","#CCDD00",visible, "THE_TYPE_OF_DATA", "THE_DATE_PROPERTY", "THE_DISPLAY_PROPERTY");
        SomeService service = new SomeService();
        entryGroup.setItems(someService.getItems("THE_TYPE_OF_DATA", "THE_DATE_PROPERTY"));
        getSchedulerHandler().addGroup(entryGroup);
}

And as you can see the createModel()-method loops the map, and if the group is visible it adds all the items of this group as entries to the calendar. I call createModel() whenever a group is updated or added/deleted, as well as if the selected date of the calendar changes. The “setEntryGroupToDisplay/Hide/Delete()”-methods are used from a jsp/menu to be able to manage the content of the calendar. To hide a group e.g. you could do it very easily with a commandLink and an updateActionListener:


<h:commandLink id="hideLink"  styleClass="myLink" rendered="#{entryGroup.visible}" action="#{schedulerHandler.hideEntryGroup}"  >
          <t:graphicImage value="/images/checkBoxChecked.gif" border="0"></t:graphicImage>
          <t:updateActionListener  value="#{entryGroup}"  property="#{schedulerHandler.entryGroupToHide}"></t:updateActionListener>
</h:commandLink>

Following this example you should now be able to use your calendar with grouped entries. Nice!

But in the beginning of this post I said that I didn’t want to retrieve more items from my backend than I needed to. As the code is now, all the items in the list of the entryGroup are added to the model, and the entryGroup is created before it is aware of which date the calender is currently displaying. This means that it’s impossible to know which items to retreive, as you don’t know which ones are needed.To give the entryGroup knowledge of this, I added a property called dateToFetchFrom, and I move the responsibility of filling up the items-list inside the group-object itself. This is solved by the “fetchItems()”-method. In the calendar-view, if you display a month, it might also contain some days of the previous month as well as the next month. Because of that the date-range I decided to retrieve items for is those three months, the current month, and that month -1 and that month + 1. My SchedulerEntryGroup then became like this:


public class SchedulerEntryGroup
{
    private String name;
    private String dataTypeName;
    private List<Item> items;
    private String color;
    private boolean visible;
    private String dateProperty;
    private String displayProperty;
    private Date dateToFetchFrom;

   public SchedulerEntryGroup()
    {
        dateToFetchFrom = new Date();
	items = new ArrayList<Items>();
    }

    publc SchedulerEntryGroup(String name, String color, boolean visible, String dataTypeName, String dateProperty,String displayProperty)
    {
        setName(name);
        setColor(color);
        setVisible(visible);
        setDateProperty(dateProperty);
        setDisplayProperty(displayProperty);
        setDataTypeName(dataTypeName);

    }

    public void fetchItems()
	{
		Calendar cal = new GregorianCalendar();
		cal.setTime(getDateToFetchFrom());

		cal.add(Calendar.MONTH, -1);
		Date fetchFrom = cal.getTime();
		cal.add(Calendar.MONTH, +1);
		Date fetchTo = cal.getTime();

		List<Item> itemsFetched = new ArrayList<Item>();
		SomeBackendService service = new SomeBackendService();
		service.getItems(getDataTypeName(), getDateProperty(), getDisplayProperty(), fetchFrom, fetchTo );
		setItems(itemsFetched);
	}
   .........
   SET/GET METHODS
   ......... 

}

Of course we then need to modify the createModel() of the SchedulerHandler as well, as we need to take use of the new dateToFetchFrom property as well as the fetchItems()-method. The modified version of createModel() is like this:

private void createModel()
    {
    	setModel(new SimpleScheduleModel());
    	int i = 0;
    	String id = "";
    	for(Entry<String, SchedulerEntryGroup> e : schedulerMap.entrySet())
    	{
    		id = "Entry_"+e.getValue().getName()+i;

    		SchedulerEntryGroup entryGroup = e.getValue();
    		if(entryGroup.isVisible())
    		{

    			entryGroup.setDateToFetchFrom(getSelectedDate());
    			entryGroup.fetchItems();

			List<Item> items = entryGroup.getItems();

                for(Item i : items)
                {
                    SchedulerEntryExtended entry = new SchedulerEntryExtended();
                    entry.setColor(entryGroup.getColor());

                     Object o = i.getProperty(entryGroup.getDateProperty());

                     String s = (String) o;
                     Date d = null;
                     if(StringUtils.isNotBlank(s))
                     {
                        try
                        {
                            d = DateUtil.parseDate((String)o);
                        } catch (ParseException pe)
                        {
                            // TODO Auto-generated catch block
                            pe.printStackTrace();
                        }
                        model.setSelectedDate(d);
                        entry.setId(id);
                        entry.setTitle(i.getProperty(entryGroup.getDisplayProperty()));

                        entry.setStartTime(d);
                        entry.setEndTime(d);
                        entry.setAllDay(true);
                        model.addEntry(entry);
                     }
                     i++;
                }
           }
        }

        if(getSelectedDate() != null)
            model.setSelectedDate(getSelectedDate());
        else
            model.setSelectedDate(new Date());
        model.setMode(3);
        model.refresh();
    }

We see that for each group that is visible, we set the selectedDate of the calendar, and we then execute the fetchItems()-method based on this. If somebody changes the date of the calendar, createModel() is called again, and a new date-range will be the criteria for retrieving items.

As a last comment on the scheduler, I can say that I was troubled by some css-styling and that the borders of the scheduler didn’t render correctly. In my web application I have a style-property on the table tag that says “border-collapse: collapse”. This also effected the rendering of the calendar in Firefox, there were no borders around each day, so the view looked a bit messy. For other reasons I could not remove or change the table tag, so I had to edit the stylesheet of the calendar myself. In the library this can be found under tomahawk-1.1.6.jar\org\apache\myfaces\custom\schedule\resource\css and is named schedule.css. I made the following change to apply separate borders for tables created inside the scheduler only (apply the style to all tables that are children of the div named schedule-compact-default):


div[class=schedule-compact-default]>table
{
border-collapse: separate !important;
}

Ok, I hope that this example have given you some tips on how you can customize the scheduler component. Good luck!