It may not be that obvious but services are at play all over Tapestry. The Page service is called when a user navigates to a particular page and asset service is called to get an image or css file. At some point it is quite likely you will need to write you own to perform some job. Typically this is the case when you want to stream some non-markup data back to the client.
Writing your own is typically considered advanced Tapestry, mainly because some of the processes involved are not that obvious. This section will hopefully shed a little light on how to wire up your own service in Tapestry 4.
I'll use an example I've written that uses a number of concepts and continues the Export service example from the Tapestry components section.
I've written a Base Service class to encapsulate a number of pieces of functionality that all sub-class services might want to capitalise on.
public abstract class BaseService implements IEngineService, IExportService { protected transient Log log = LogFactory.getLog(getClass()); private HttpServletRequest request; private HttpServletResponse response; private LinkFactory linkFactory; private ApplicationStateManager applicationStateManager; public static final String VISIT_OBJECT = "visit"; /** * This will be autowired from HiveMind...chk hivemodule.xml * @param asm */ public void setApplicationStateManager(ApplicationStateManager asm) { this.applicationStateManager = asm; } public Visit getVisitObject() throws ServiceException { if (applicationStateManager != null) { Visit visit = null; try { visit = (Visit) this.applicationStateManager.get(BaseService.VISIT_OBJECT); } catch (Exception e) { throw new ServiceException("Could not load ASO: " + BaseService.VISIT_OBJECT + " " + e.getMessage()); } return visit; } else { throw new ServiceException("Could not load ASO: " + BaseService.VISIT_OBJECT); } } public HttpServletRequest getRequest() { return request; } public HttpServletResponse getResponse() { return response; } public LinkFactory getLinkFactory() { return linkFactory; } public ApplicationStateManager getApplicationStateManager() { return applicationStateManager; } /** * This will be autowired from HiveMind...chk hivemodule.xml * @param linkFactory */ public void setLinkFactory(LinkFactory linkFactory) { this.linkFactory = linkFactory; } public void setResponse(HttpServletResponse response) { this.response = response; } public void setRequest(HttpServletRequest request) { this.request = request; }
There are a number of member variables here and they are all set by HiveMind. When you declare this service in HiveMind, HiveMind looks at how it can set the values automatically.
Eg.
public void setLinkFactory(LinkFactory linkFactory) { this.linkFactory = linkFactory; }
This is set at runtime from the Tapestry declared (in its own Hivemind registry) linkFactory. It has to be declared in our Hive module (so is not fully "auto-wired") because there are more than one Tapestry LinkFactories (I believe the other one is for portlets?)
<set-object property="linkFactory" value="service:tapestry.url.LinkFactory"/>
<!--Export Service--> <contribution configuration-id="tapestry.services.ApplicationServices"> <service name="ExportService" object="service:mymodule.ExportService" /> </contribution> <service-point id="ExportService" interface="org.crud.services.IExportService"> <invoke-factory> <construct class="org.crud.services.ExportService"> <set-object property="linkFactory" value="service:tapestry.url.LinkFactory"/> <set-object property="applicationStateManager" value="service:tapestry.state.ApplicationStateManager"/> </construct> </invoke-factory> </service-point>
public class ExportService extends BaseService { public ILink getLink(boolean post, Object parameter) { // In order to have the params squeezed properly you have to do this... Map<String, Object[]> serviceParams = new HashMap<String, Object[]>(); serviceParams.put(ServiceConstants.PARAMETER, (Object[]) parameter); ILink iLink = getLinkFactory().constructLink(this, post, serviceParams, false); return iLink; } public void service(IRequestCycle cycle) throws IOException { ExportParam exportParam = (ExportParam) getLinkFactory().extractListenerParameters(cycle)[0]; try { createReport(exportParam); } catch (ServiceException e) { } }
The main thing here is that any parameters your service needs need to be passed around as POST or GET data so there will be a size restriction of the parameter data based on your browser and app server. You get so used to calling methods in Tapestry that it is hard to get used to posting data around again, but this is lower level stuff so we have to deal with it ourselves. My solution (and there is bound to be better) is to create a special Parameter class (ExportParam in this example). But to add any arbitrary object as a parameter you have to put the service parameters like so:
serviceParams.put(ServiceConstants.PARAMETER, (Object[]) parameter);
In the simpler case where you just want to pass Strings you do this for example:
parameters.put(ServiceConstants.SERVICE, getName()); parameters.put(ServiceConstants.PAGE, component.getPage().getPageName()); parameters.put(ServiceConstants.COMPONENT, component.getIdPath());
The parameter class ExportParam is from the need to call JasperReports so the parameter has a report name, an export type (eg PDF, csv) and a set of parameters that the report will pass to its call to a stored procedure.
public class ExportParam implements Serializable { // Params to pass to Jasper private Map reportParams; private String reportName; private MimeType exportType; ... // Getters and setters for each...
Recall that when the user clicks the "Go" button a listener method is run that throws a Rediect to the browser with the URL of the service. The page class is responsible for gathering parameters and getting the service to generate its URL.
// Inject the service @InjectObject("service:mymodule.ExportService") public abstract IExportService getExportService(); // Redirect to the service public void formSubmit() { throw new RedirectException(generateExportUrl()); } // Get the URL of the service complete with parameters so it can run as expected. public String generateExportUrl() { // Get link as regular link, not a form post and pass params ExportParam exportParams = new ExportParam(); exportParams.setExportType(getExportType()); exportParams.setReportName("MyReport"); exportParams.setReportParams(gatherReportParams()); Object[] params = new Object[]{exportParams}; return getExportService().getLink(false, params).getURL(); }
...and you thought it would be difficult