D. How to customize a UWS ?

6. Actions

At each sent request, the UWS identifies the corresponding action (i.e. listing jobs, starting, creating or displaying a job, ...) and executes it. This library allows you to add and/or remove actions to/from your UWS. So in this part of the tutorial you will learn how to define your own action(s) and how to add it (them) to your UWS.

The class UWSAction

So that offering more flexibility, a UWS contains an extendable set of actions. Each one is identified by a unique name and is able to indicate whether it can be applied considering a given HTTP request. So when receiving a request, a UWS loops on all its available actions and chooses the one which matches. The match condition and the action execution are both defined in one class which must extend UWSAction.

To sum up, an extension of UWSAction must define two functions:

Obviously all UWS actions described by the IVOA Recommendation are already implemented. Here is their corresponding class:

ShowHomePage is also a sub-class of UWSAction, but is not an action described by the IVOA Recommendation. This action corresponds to the URI: /{uws} (the base UWS URL actually).

Below is the class diagram of all existing actions:

How does it work ?

AbstractUWS is an ordered set of UWSActions. Its attribute uwsActions groups together all its possible actions. When a user sends a request to the servlet, the function AbstractUWS.executeRequest(...) is called. This one will choose the action corresponding to the received HTTP request by iterating in uwsActions:

public abstract class AbstractUWS<JL extends JobList<J>, J extends AbstractJob> {
...
	public boolean executeRequest(HttpServletRequest request, HttpServletResponse response) throws UWSException, IOException {
	...
			boolean actionFound = false;
			for(int i=0; !actionFound && i<uwsActions.size(); i++){
				if (uwsActions.get(i).match(urlInterpreter, userId, request)){
					actionFound = true;
					actionApplied = uwsActions.get(i).apply(urlInterpreter, userId, request, response);
				}
			}
			
			if (!actionFound)
				throw new UWSException(UWSException.NOT_IMPLEMENTED, "[Execute UWS request] This UWS action is not supported by this UWS service !");
	...
	}
...
}

As you can notice, the actions of a UWS are evalutated in the order with their function UWSAction.match(UWSUrl, String, HttpServletRequest). If it returns true the method apply(UWSUrl, String, HttpServletRequest, HttpServletResponse) is called to execute the action. If false we try with the next action, and so on.

How to customize ?

UWSAction is an abstract class: the functions match(UWSUrl, String, HttpServletRequest) and apply(UWSUrl, String, HttpServletRequest, HttpServletResponse) must be overrided. Besides the attribute uwsActions of AbstractUWS is a vector and several getters and setters let managing easily the list of actions available for a given UWS. Thus to customize this part of your UWS you have to:

  1. Extend UWSAction (or one of its sub-classes)
  2. Update your UWS

To illustrate a such customization lets see an example with an action named AboutAction. This additional action has to display some information about a UWS service (name, description, number of jobs lists, ...) but also some statistics about its jobs.

1. Extend UWSAction

Any type of UWS action must have a name, a kind of action ID. So this name must be unique for each type of action, that is to say for each extension of UWSAction. The name does not have to change at each instantiation, it is used to distinguish the different type of action in a given UWS ! It is returned by the function getName(), which is not abstract, but returns by default the absolute name of the java class.

The name of the default UWS actions are stored as final class variable in UWSAction:

The name of our additional action - AboutAction - will be:

public class AboutAction<JL extends JobList<J>, J extends AbstractJob> extends UWSAction<JL, J> {

	public AboutAction(AbstractUWS<JL, J> u) {
		super(u);
	}

	@Override
	public String getName() {
		return "About UWS";
	}
...
}

After the name of the action you have to define at which condition it must be applied: the function UWSAction.match(UWSUrl, String, HttpServletRequest). You must be very careful when overriding this function ! Indeed the tested condition has to be as precise as possible so that avoiding to forget this action or to trigger this action rather than another one. For instance, the difference between ListJobs and AddJob is thin: the HTTP method (GET for ListJobs and POST for AddJob). To avoid the confusion the HTTP method must absolutely be tested.

Here is their match(...) function:

public class AddJob<JL extends JobList<J>, J extends AbstractJob> extends UWSAction<JL, J> {
	public boolean match(UWSUrl urlInterpreter, String userId, HttpServletRequest request) throws UWSException {
		return (urlInterpreter.getJobListName() != null				// jobs list specified
				&& urlInterpreter.getJobId() == null				// no job specified
				&& request.getMethod().equalsIgnoreCase("post"));	// HTTP-POST
	}
...
}

public class ListJobs<JL extends JobList<J>, J extends AbstractJob> extends UWSAction<JL, J> {
	public boolean match(UWSUrl urlInterpreter, String userId, HttpServletRequest request) throws UWSException {
		return (urlInterpreter.getJobListName() != null				// jobs list specified
				&& urlInterpreter.getJobId() == null				// no job specified
				&& request.getMethod().equalsIgnoreCase("get"));	// HTTP-GET
	}
...
}

In our example, AboutAction will be applied when the URL is the base UWS URL and only when the parameter ACTION has the value ABOUT. The HTTP method does not matter here, because the parameter is enough to distinguish this action from ShowHomePage (which has no parameter).

public class AboutAction<JL extends JobList<J>, J extends AbstractJob> extends UWSAction<JL, J> {
...
	@Override
	public boolean match(UWSUrl urlInterpreter, String userId, HttpServletRequest request) throws UWSException {
		return urlInterpreter.getJobListName() == null							// no jobs list specified
				&& request.getParameter("ACTION") != null						// at least, 1 parameter = ACTION
				&& request.getParameter("ACTION").equalsIgnoreCase("about");	// its value must be ABOUT (not case sensitive)
	}
...
}

And finally: the function apply(UWSUrl, String, HttpServletRequest, HttpServletResponse). AboutAction must write a HTML page with some information about the UWS service:

Here is the full source code:

public class AboutAction<JL extends JobList<J>, J extends AbstractJob> extends UWSAction<JL, J> {
...
	@Override
	public boolean apply(UWSUrl urlInterpreter, String userId, HttpServletRequest request, HttpServletResponse response) throws UWSException, IOException {
		response.setContentType("text/html");
		
		PrintWriter out = response.getWriter();
		
		BufferedReader reader = new BufferedReader(new FileReader(request.getSession().getServletContext().getRealPath("/about_header.txt")));
		try{
			String line = null;
			while((line = reader.readLine()) != null)
				out.println(line);
		}finally { reader.close(); }
		
		out.println("<h1>About the UWS Algorithms</h1>");
		
		out.println("<h2>UWS Algorithms</h2>");
		out.println("<ul>");
		out.println("<li><b>Name:</b> "+uws.getName()+"</li>");
		out.println("<li><b>Description:</b><p style=\"margin-bottom: 0; padding-bottom: 0\">"+uws.getDescription()+"</p></li>");
		out.println("<li><b>Base UWS URL:</b> "+uws.getBaseURL()+"</li><br />");
		out.println("<li><b>Nb Queued Jobs:</b> "+uws.getNbQueuedJobs()+"</li>");
		
		out.print("<li><b>Nb Running Jobs:</b> "+uws.getNbRunningJobs());
		if (uws.getNbRunningJobs() > 0){
			Iterator<J> it = uws.getRunningJobs();
			String runningJobs = null;
			while(it.hasNext())
				runningJobs = ((runningJobs==null)?"":(runningJobs+", "))+it.next().getJobId();
			out.print(" ("+runningJobs+")");
		}
		out.println("</li>");
		
		out.println("<li><b>Nb Max Running Jobs:</b> 3</li>");
		out.println("<li><b>Nb Jobs Lists:</b> "+uws.getNbJobList()+"</li>");
		out.println("</ul>");
		
		out.println("<h2>"+uws.getNbSerializers()+" Available Serializers</h2>");
		out.println("<ul>");
		Iterator<UWSSerializer> itSerializers = uws.getSerializers();
		while(itSerializers.hasNext()){
			UWSSerializer serializer = itSerializers.next();
			out.println("<li><b>"+serializer.getMimeType()+"</b></li>");
		}
		out.println("</ul>");
		
		out.println("<h2>"+uws.getNbUWSActions()+" Available Actions</h2>");
		out.println("<ul>");
		Iterator<UWSAction<JL,J>> itActions = uws.getUWSActions();
		while(itActions.hasNext()){
			UWSAction<JL,J> action = itActions.next();
			out.println("<li><b>"+action.getName()+"</b><p style=\"margin-bottom: 0; padding-bottom: 0\"><i>"+action.getDescription()+"</i></p></li>");
		}
		out.println("</ul>");

		out.println("<h2>Jobs Lists</h2>");
		out.println("<table style=\"text-align: center; width: 100%;\">");
		out.println("<tr><th></th><th>Users</th><th>Jobs</th><th>Pending</th><th>Queued</th><th>Running</th><th>Complete</th><th>Error</th><th>Aborted</th><th>Others</th></tr>");
		for(JL jl : uws){
			int nbPending=0, nbQueued=0, nbRunning=0, nbComplete=0, nbError=0, nbAborted=0, nbOthers=0;
			for(J job : jl){
				switch(job.getPhase()){
					case PENDING: nbPending++; break;
					case QUEUED: nbQueued++; break;
					case EXECUTING: nbRunning++; break;
					case COMPLETED: nbComplete++; break;
					case ERROR: nbError++; break;
					case ABORTED: nbAborted++; break;
					default: nbOthers++; break;
				}
			}
			out.println("<tr><td><b><a href=\"extended/"+jl.getName()+"\">"+jl.getName()+"</a></b></td><td>"+(jl.getNbUsers()<=0?"-":jl.getNbUsers())+"</td><td>"+(jl.getNbJobs()<=0?"-":jl.getNbJobs())+"</td><td>"+(nbPending<=0?"-":nbPending)+"</td><td>"+(nbQueued<=0?"-":nbQueued)+"</td><td>"+(nbRunning<=0?"-":nbRunning)+"</td><td>"+(nbComplete<=0?"-":nbComplete)+"</td><td>"+(nbError<=0?"-":nbError)+"</td><td>"+(nbAborted<=0?"-":nbAborted)+"</td><td>"+(nbOthers<=0?"-":nbOthers)+"</td></tr>");
		}
		out.println("</table>");
		
		reader = new BufferedReader(new FileReader(request.getSession().getServletContext().getRealPath("about_footer.txt")));
		try{
			String line = null;
			while((line = reader.readLine()) != null)
				out.println(line);
		}finally { reader.close(); }
		
		out.close();
		
		return true;
	}
...
}

2. Update your UWS

Once finished, your extension of UWSAction can be added/setted in your UWS. For that you have three ways:

All these methods work ONLY IF no action with the same name already exists in the UWS ! The only exception is replaceUWSAction(UWSAction) whose the goal is to replace a UWSAction by another one with the same name.

Below is how the action AboutAction is inserted in a UWS service:

public class MyExtendedUWS extends ExtendedUWS {

	public MyExtendedUWS(URL baseURL) throws UWSException {
		super(baseURL);
		addUWSAction(0, new AboutAction<JobList<AbstractJob>, AbstractJob>(this));
	}
...
}

Be very careful with the action position !

The action has been inserted at the first position, to be sure it will be always evaluated ! Here it is necessary that this action is tested, at least, before ShowHomePage, because ShowHomePage matches even if there are parameters.

Obviously in AbstractUWS you can also: